]> OCCT Git - occt.git/commitdiff
Modeling Data - Add GeomHash and Geom2dHash packages (#845)
authorPasukhin Dmitry <dpasukhi@opencascade.com>
Sat, 22 Nov 2025 11:16:25 +0000 (11:16 +0000)
committerGitHub <noreply@github.com>
Sat, 22 Nov 2025 11:16:25 +0000 (11:16 +0000)
- Implementation of hashers for analytic curves (Line, Circle, Ellipse, Hyperbola, Parabola) and freeform curves (Bezier, BSpline, Trimmed, Offset) in both 2D and 3D
- Implementation of hashers for surfaces including elementary surfaces (Plane, Cylinder, Cone, Sphere, Torus) and derived surfaces (Revolution, LinearExtrusion, RectangularTrimmed, Offset)
- Comprehensive test coverage for all hasher implementations

51 files changed:
src/ModelingData/TKG2d/GTests/FILES.cmake
src/ModelingData/TKG2d/GTests/Geom2dHash_CurveHasher_Test.cxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/FILES.cmake [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_AxisPlacement.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BSplineCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BezierCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CircleHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.cxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.hxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_DirectionHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_EllipseHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_HyperbolaHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_LineHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_OffsetCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_ParabolaHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_PointHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_TrimmedCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG2d/PACKAGES.cmake
src/ModelingData/TKG3d/GTests/FILES.cmake
src/ModelingData/TKG3d/GTests/GeomHash_CurveHasher_Test.cxx [new file with mode: 0644]
src/ModelingData/TKG3d/GTests/GeomHash_SurfaceHasher_Test.cxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/FILES.cmake [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_AxisPlacement.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_BezierCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_BezierSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_CircleHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_ConicalSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.cxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.hxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_CylindricalSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_DirectionHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_EllipseHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_HyperbolaHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_LineHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_ParabolaHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_PlaneHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_PointHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_RectangularTrimmedSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_SphericalSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.cxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.hxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfLinearExtrusionHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfRevolutionHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_ToroidalSurfaceHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_TrimmedCurveHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/GeomHash/GeomHash_VectorHasher.pxx [new file with mode: 0644]
src/ModelingData/TKG3d/PACKAGES.cmake

index db61773428f57e313e2103363e004e760ba1bc17..42478599b5091b1b7efcddc1b8cc790f5a36d620 100644 (file)
@@ -8,4 +8,5 @@ set(OCCT_TKG2d_GTests_FILES
   Geom2dAPI_InterCurveCurve_Test.cxx
   Geom2dGcc_Circ2d2TanOn_Test.cxx
   Geom2dGcc_Circ2d2TanRad_Test.cxx
+  Geom2dHash_CurveHasher_Test.cxx
 )
diff --git a/src/ModelingData/TKG2d/GTests/Geom2dHash_CurveHasher_Test.cxx b/src/ModelingData/TKG2d/GTests/Geom2dHash_CurveHasher_Test.cxx
new file mode 100644 (file)
index 0000000..b858b17
--- /dev/null
@@ -0,0 +1,654 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <gtest/gtest.h>
+
+#include <Geom2dHash_CurveHasher.hxx>
+#include <Geom2d_Line.hxx>
+#include <Geom2d_Circle.hxx>
+#include <Geom2d_Ellipse.hxx>
+#include <Geom2d_Hyperbola.hxx>
+#include <Geom2d_Parabola.hxx>
+#include <Geom2d_BezierCurve.hxx>
+#include <Geom2d_BSplineCurve.hxx>
+#include <Geom2d_TrimmedCurve.hxx>
+#include <Geom2d_OffsetCurve.hxx>
+#include <gp_Ax2d.hxx>
+#include <gp_Ax22d.hxx>
+#include <gp_Pnt2d.hxx>
+#include <gp_Dir2d.hxx>
+#include <gp_Vec2d.hxx>
+#include <TColgp_Array1OfPnt2d.hxx>
+#include <TColStd_Array1OfReal.hxx>
+#include <TColStd_Array1OfInteger.hxx>
+
+class Geom2dHash_CurveHasherTest : public ::testing::Test
+{
+protected:
+  Geom2dHash_CurveHasher myHasher;
+};
+
+// ============================================================================
+// Line Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Line_CopiedLines_SameHash)
+{
+  gp_Pnt2d            aLoc(1.0, 2.0);
+  gp_Dir2d            aDir(1.0, 0.0);
+  Handle(Geom2d_Line) aLine1 = new Geom2d_Line(aLoc, aDir);
+  Handle(Geom2d_Line) aLine2 = Handle(Geom2d_Line)::DownCast(aLine1->Copy());
+
+  EXPECT_EQ(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_TRUE(myHasher(aLine1, aLine2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Line_DifferentLines_DifferentHash)
+{
+  Handle(Geom2d_Line) aLine1 = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_Line) aLine2 = new Geom2d_Line(gp_Pnt2d(1.0, 0.0), gp_Dir2d(1.0, 0.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+// ============================================================================
+// Circle Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_CopiedCircles_SameHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 5.0);
+  Handle(Geom2d_Circle) aCircle2 = Handle(Geom2d_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_DifferentRadius_DifferentHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 5.0);
+  Handle(Geom2d_Circle) aCircle2 = new Geom2d_Circle(anAxis, 10.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+// ============================================================================
+// Ellipse Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Ellipse_CopiedEllipses_SameHash)
+{
+  gp_Ax22d               anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Ellipse) anEllipse1 = new Geom2d_Ellipse(anAxis, 10.0, 5.0);
+  Handle(Geom2d_Ellipse) anEllipse2 = Handle(Geom2d_Ellipse)::DownCast(anEllipse1->Copy());
+
+  EXPECT_EQ(myHasher(anEllipse1), myHasher(anEllipse2));
+  EXPECT_TRUE(myHasher(anEllipse1, anEllipse2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Ellipse_DifferentRadii_DifferentHash)
+{
+  gp_Ax22d               anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Ellipse) anEllipse1 = new Geom2d_Ellipse(anAxis, 10.0, 5.0);
+  Handle(Geom2d_Ellipse) anEllipse2 = new Geom2d_Ellipse(anAxis, 10.0, 7.0);
+
+  EXPECT_NE(myHasher(anEllipse1), myHasher(anEllipse2));
+  EXPECT_FALSE(myHasher(anEllipse1, anEllipse2));
+}
+
+// ============================================================================
+// Hyperbola Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Hyperbola_CopiedHyperbolas_SameHash)
+{
+  gp_Ax22d                 anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Hyperbola) aHyp1 = new Geom2d_Hyperbola(anAxis, 5.0, 3.0);
+  Handle(Geom2d_Hyperbola) aHyp2 = Handle(Geom2d_Hyperbola)::DownCast(aHyp1->Copy());
+
+  EXPECT_EQ(myHasher(aHyp1), myHasher(aHyp2));
+  EXPECT_TRUE(myHasher(aHyp1, aHyp2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Hyperbola_DifferentRadii_DifferentHash)
+{
+  gp_Ax22d                 anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Hyperbola) aHyp1 = new Geom2d_Hyperbola(anAxis, 5.0, 3.0);
+  Handle(Geom2d_Hyperbola) aHyp2 = new Geom2d_Hyperbola(anAxis, 5.0, 4.0);
+
+  EXPECT_NE(myHasher(aHyp1), myHasher(aHyp2));
+  EXPECT_FALSE(myHasher(aHyp1, aHyp2));
+}
+
+// ============================================================================
+// Parabola Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Parabola_CopiedParabolas_SameHash)
+{
+  gp_Ax22d                anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Parabola) aPar1 = new Geom2d_Parabola(anAxis, 2.0);
+  Handle(Geom2d_Parabola) aPar2 = Handle(Geom2d_Parabola)::DownCast(aPar1->Copy());
+
+  EXPECT_EQ(myHasher(aPar1), myHasher(aPar2));
+  EXPECT_TRUE(myHasher(aPar1, aPar2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Parabola_DifferentFocal_DifferentHash)
+{
+  gp_Ax22d                anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Parabola) aPar1 = new Geom2d_Parabola(anAxis, 2.0);
+  Handle(Geom2d_Parabola) aPar2 = new Geom2d_Parabola(anAxis, 3.0);
+
+  EXPECT_NE(myHasher(aPar1), myHasher(aPar2));
+  EXPECT_FALSE(myHasher(aPar1, aPar2));
+}
+
+// ============================================================================
+// BezierCurve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BezierCurve_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 2.0);
+  aPoles(3) = gp_Pnt2d(3.0, 2.0);
+  aPoles(4) = gp_Pnt2d(4.0, 0.0);
+
+  Handle(Geom2d_BezierCurve) aCurve1 = new Geom2d_BezierCurve(aPoles);
+  Handle(Geom2d_BezierCurve) aCurve2 = Handle(Geom2d_BezierCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, BezierCurve_DifferentPoles_DifferentComparison)
+{
+  TColgp_Array1OfPnt2d aPoles1(1, 3);
+  aPoles1(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles1(2) = gp_Pnt2d(1.0, 2.0);
+  aPoles1(3) = gp_Pnt2d(2.0, 0.0);
+
+  TColgp_Array1OfPnt2d aPoles2(1, 3);
+  aPoles2(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles2(2) = gp_Pnt2d(1.0, 3.0); // Different
+  aPoles2(3) = gp_Pnt2d(2.0, 0.0);
+
+  Handle(Geom2d_BezierCurve) aCurve1 = new Geom2d_BezierCurve(aPoles1);
+  Handle(Geom2d_BezierCurve) aCurve2 = new Geom2d_BezierCurve(aPoles2);
+
+  // Hash may be same (metadata only), but comparison should differ
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// BSplineCurve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 1.0);
+  aPoles(4) = gp_Pnt2d(3.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 = new Geom2d_BSplineCurve(aPoles, aKnots, aMults, 3);
+  Handle(Geom2d_BSplineCurve) aCurve2 = Handle(Geom2d_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// TrimmedCurve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, TrimmedCurve_CopiedCurves_SameHash)
+{
+  Handle(Geom2d_Line)         aLine     = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_TrimmedCurve) aTrimmed1 = new Geom2d_TrimmedCurve(aLine, 0.0, 10.0);
+  Handle(Geom2d_TrimmedCurve) aTrimmed2 = Handle(Geom2d_TrimmedCurve)::DownCast(aTrimmed1->Copy());
+
+  EXPECT_EQ(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_TRUE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, TrimmedCurve_DifferentBounds_DifferentHash)
+{
+  Handle(Geom2d_Line)         aLine     = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_TrimmedCurve) aTrimmed1 = new Geom2d_TrimmedCurve(aLine, 0.0, 10.0);
+  Handle(Geom2d_TrimmedCurve) aTrimmed2 = new Geom2d_TrimmedCurve(aLine, 0.0, 20.0);
+
+  EXPECT_NE(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_FALSE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+// ============================================================================
+// OffsetCurve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, OffsetCurve_CopiedCurves_SameHash)
+{
+  Handle(Geom2d_Line)        aLine     = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_OffsetCurve) anOffset1 = new Geom2d_OffsetCurve(aLine, 5.0);
+  Handle(Geom2d_OffsetCurve) anOffset2 = Handle(Geom2d_OffsetCurve)::DownCast(anOffset1->Copy());
+
+  EXPECT_EQ(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_TRUE(myHasher(anOffset1, anOffset2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, OffsetCurve_DifferentOffset_DifferentHash)
+{
+  Handle(Geom2d_Line)        aLine     = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_OffsetCurve) anOffset1 = new Geom2d_OffsetCurve(aLine, 5.0);
+  Handle(Geom2d_OffsetCurve) anOffset2 = new Geom2d_OffsetCurve(aLine, 10.0);
+
+  EXPECT_NE(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_FALSE(myHasher(anOffset1, anOffset2));
+}
+
+// ============================================================================
+// Cross-type Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, DifferentTypes_DifferentComparison)
+{
+  Handle(Geom2d_Line)   aLine = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle = new Geom2d_Circle(anAxis, 5.0);
+
+  EXPECT_FALSE(myHasher(aLine, aCircle));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, NullCurves_HandledCorrectly)
+{
+  Handle(Geom2d_Curve) aNullCurve;
+  Handle(Geom2d_Line)  aLine = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+
+  EXPECT_EQ(myHasher(aNullCurve), 0u);
+  EXPECT_TRUE(myHasher(aNullCurve, aNullCurve));
+  EXPECT_FALSE(myHasher(aLine, aNullCurve));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, SameObject_Equal)
+{
+  Handle(Geom2d_Line) aLine = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  EXPECT_TRUE(myHasher(aLine, aLine));
+}
+
+// ============================================================================
+// Weighted BSpline Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_Weighted_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 1.0);
+  aPoles(4) = gp_Pnt2d(3.0, 0.0);
+
+  TColStd_Array1OfReal aWeights(1, 4);
+  aWeights(1) = 1.0;
+  aWeights(2) = 2.0;
+  aWeights(3) = 2.0;
+  aWeights(4) = 1.0;
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 =
+    new Geom2d_BSplineCurve(aPoles, aWeights, aKnots, aMults, 3);
+  Handle(Geom2d_BSplineCurve) aCurve2 = Handle(Geom2d_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_DifferentWeights_DifferentComparison)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 1.0);
+  aPoles(4) = gp_Pnt2d(3.0, 0.0);
+
+  TColStd_Array1OfReal aWeights1(1, 4);
+  aWeights1(1) = 1.0;
+  aWeights1(2) = 2.0;
+  aWeights1(3) = 2.0;
+  aWeights1(4) = 1.0;
+
+  TColStd_Array1OfReal aWeights2(1, 4);
+  aWeights2(1) = 1.0;
+  aWeights2(2) = 3.0; // Different
+  aWeights2(3) = 2.0;
+  aWeights2(4) = 1.0;
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 =
+    new Geom2d_BSplineCurve(aPoles, aWeights1, aKnots, aMults, 3);
+  Handle(Geom2d_BSplineCurve) aCurve2 =
+    new Geom2d_BSplineCurve(aPoles, aWeights2, aKnots, aMults, 3);
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Weighted Bezier Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BezierCurve_Weighted_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 3);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 2.0);
+  aPoles(3) = gp_Pnt2d(2.0, 0.0);
+
+  TColStd_Array1OfReal aWeights(1, 3);
+  aWeights(1) = 1.0;
+  aWeights(2) = 2.0;
+  aWeights(3) = 1.0;
+
+  Handle(Geom2d_BezierCurve) aCurve1 = new Geom2d_BezierCurve(aPoles, aWeights);
+  Handle(Geom2d_BezierCurve) aCurve2 = Handle(Geom2d_BezierCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Axis Orientation Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_DifferentAxisOrientation_DifferentHash)
+{
+  gp_Ax22d              anAxis1(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  gp_Ax22d              anAxis2(gp_Pnt2d(0.0, 0.0), gp_Dir2d(0.0, 1.0), gp_Dir2d(-1.0, 0.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis1, 5.0);
+  Handle(Geom2d_Circle) aCircle2 = new Geom2d_Circle(anAxis2, 5.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Line_DifferentDirection_DifferentHash)
+{
+  Handle(Geom2d_Line) aLine1 = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_Line) aLine2 = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(0.0, 1.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+// ============================================================================
+// BSpline with Different Knots Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_DifferentKnots_DifferentComparison)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 1.0);
+  aPoles(4) = gp_Pnt2d(3.0, 0.0);
+
+  TColStd_Array1OfReal aKnots1(1, 2);
+  aKnots1(1) = 0.0;
+  aKnots1(2) = 1.0;
+
+  TColStd_Array1OfReal aKnots2(1, 2);
+  aKnots2(1) = 0.0;
+  aKnots2(2) = 2.0; // Different
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 = new Geom2d_BSplineCurve(aPoles, aKnots1, aMults, 3);
+  Handle(Geom2d_BSplineCurve) aCurve2 = new Geom2d_BSplineCurve(aPoles, aKnots2, aMults, 3);
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Periodic BSpline Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_QuadraticBezierEquivalent_CopiedCurves_SameHash)
+{
+  // Simple quadratic B-spline (Bezier-like, single span)
+  TColgp_Array1OfPnt2d aPoles(1, 3);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 2.0);
+  aPoles(3) = gp_Pnt2d(2.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 3;
+  aMults(2) = 3;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 = new Geom2d_BSplineCurve(aPoles, aKnots, aMults, 2);
+  Handle(Geom2d_BSplineCurve) aCurve2 = Handle(Geom2d_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Reversed Curve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Line_Reversed_DifferentComparison)
+{
+  Handle(Geom2d_Line) aLine1 = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_Line) aLine2 = Handle(Geom2d_Line)::DownCast(aLine1->Reversed());
+
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, BezierCurve_Reversed_DifferentComparison)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 3);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 2.0);
+  aPoles(3) = gp_Pnt2d(2.0, 0.0);
+
+  Handle(Geom2d_BezierCurve) aCurve1 = new Geom2d_BezierCurve(aPoles);
+  Handle(Geom2d_BezierCurve) aCurve2 = Handle(Geom2d_BezierCurve)::DownCast(aCurve1->Reversed());
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Transformed Curve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_Translated_DifferentHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 5.0);
+  Handle(Geom2d_Circle) aCircle2 = Handle(Geom2d_Circle)::DownCast(aCircle1->Copy());
+  aCircle2->Translate(gp_Vec2d(1.0, 0.0));
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_Scaled_DifferentHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 5.0);
+  Handle(Geom2d_Circle) aCircle2 = Handle(Geom2d_Circle)::DownCast(aCircle1->Copy());
+  aCircle2->Scale(gp_Pnt2d(0.0, 0.0), 2.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+// ============================================================================
+// Higher Degree BSpline Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_HigherDegree_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt2d aPoles(1, 6);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 0.5);
+  aPoles(4) = gp_Pnt2d(3.0, 1.5);
+  aPoles(5) = gp_Pnt2d(4.0, 0.0);
+  aPoles(6) = gp_Pnt2d(5.0, 1.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 6;
+  aMults(2) = 6;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 = new Geom2d_BSplineCurve(aPoles, aKnots, aMults, 5);
+  Handle(Geom2d_BSplineCurve) aCurve2 = Handle(Geom2d_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Multiple Knot Spans Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, BSplineCurve_LinearMultipleSpans_CopiedCurves_SameHash)
+{
+  // Linear B-spline with multiple knots (piecewise linear)
+  TColgp_Array1OfPnt2d aPoles(1, 4);
+  aPoles(1) = gp_Pnt2d(0.0, 0.0);
+  aPoles(2) = gp_Pnt2d(1.0, 1.0);
+  aPoles(3) = gp_Pnt2d(2.0, 0.0);
+  aPoles(4) = gp_Pnt2d(3.0, 1.0);
+
+  TColStd_Array1OfReal aKnots(1, 4);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+  aKnots(3) = 2.0;
+  aKnots(4) = 3.0;
+
+  TColStd_Array1OfInteger aMults(1, 4);
+  aMults(1) = 2;
+  aMults(2) = 1;
+  aMults(3) = 1;
+  aMults(4) = 2;
+
+  Handle(Geom2d_BSplineCurve) aCurve1 = new Geom2d_BSplineCurve(aPoles, aKnots, aMults, 1);
+  Handle(Geom2d_BSplineCurve) aCurve2 = Handle(Geom2d_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Cross-type Conic Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Ellipse_vs_Hyperbola_DifferentComparison)
+{
+  gp_Ax22d                 anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Ellipse)   anEllipse  = new Geom2d_Ellipse(anAxis, 5.0, 3.0);
+  Handle(Geom2d_Hyperbola) aHyperbola = new Geom2d_Hyperbola(anAxis, 5.0, 3.0);
+
+  EXPECT_FALSE(myHasher(anEllipse, aHyperbola));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_vs_Ellipse_DifferentComparison)
+{
+  gp_Ax22d               anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle)  aCircle   = new Geom2d_Circle(anAxis, 5.0);
+  Handle(Geom2d_Ellipse) anEllipse = new Geom2d_Ellipse(anAxis, 5.0, 5.0);
+
+  EXPECT_FALSE(myHasher(aCircle, anEllipse));
+}
+
+// ============================================================================
+// Trimmed vs Base Curve Tests
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, TrimmedCurve_vs_BaseCurve_DifferentComparison)
+{
+  Handle(Geom2d_Line)         aLine    = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_TrimmedCurve) aTrimmed = new Geom2d_TrimmedCurve(aLine, 0.0, 10.0);
+
+  EXPECT_FALSE(myHasher(aLine, aTrimmed));
+}
+
+// ============================================================================
+// Edge Cases - Degenerate/Special Values
+// ============================================================================
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_VerySmallRadius_CopiedCircles_SameHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 1e-10);
+  Handle(Geom2d_Circle) aCircle2 = Handle(Geom2d_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Circle_VeryLargeRadius_CopiedCircles_SameHash)
+{
+  gp_Ax22d              anAxis(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0), gp_Dir2d(0.0, 1.0));
+  Handle(Geom2d_Circle) aCircle1 = new Geom2d_Circle(anAxis, 1e10);
+  Handle(Geom2d_Circle) aCircle2 = Handle(Geom2d_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(Geom2dHash_CurveHasherTest, Line_AtOrigin_vs_FarFromOrigin_DifferentHash)
+{
+  Handle(Geom2d_Line) aLine1 = new Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(1.0, 0.0));
+  Handle(Geom2d_Line) aLine2 = new Geom2d_Line(gp_Pnt2d(1e10, 1e10), gp_Dir2d(1.0, 0.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
diff --git a/src/ModelingData/TKG2d/Geom2dHash/FILES.cmake b/src/ModelingData/TKG2d/Geom2dHash/FILES.cmake
new file mode 100644 (file)
index 0000000..93deddc
--- /dev/null
@@ -0,0 +1,22 @@
+# Source files for Geom2dHash package
+set(OCCT_Geom2dHash_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}")
+
+set(OCCT_Geom2dHash_FILES
+  # Foundational Hashers
+  Geom2dHash_PointHasher.pxx
+  Geom2dHash_DirectionHasher.pxx
+  Geom2dHash_AxisPlacement.pxx
+
+  # Curve Hashers
+  Geom2dHash_LineHasher.pxx
+  Geom2dHash_CircleHasher.pxx
+  Geom2dHash_EllipseHasher.pxx
+  Geom2dHash_HyperbolaHasher.pxx
+  Geom2dHash_ParabolaHasher.pxx
+  Geom2dHash_BezierCurveHasher.pxx
+  Geom2dHash_BSplineCurveHasher.pxx
+  Geom2dHash_TrimmedCurveHasher.pxx
+  Geom2dHash_OffsetCurveHasher.pxx
+  Geom2dHash_CurveHasher.hxx
+  Geom2dHash_CurveHasher.cxx
+)
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_AxisPlacement.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_AxisPlacement.pxx
new file mode 100644 (file)
index 0000000..d5592e3
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_AxisPlacement_HeaderFile
+#define _Geom2dHash_AxisPlacement_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Ax22d.hxx>
+#include <Geom2dHash_PointHasher.pxx>
+#include <Geom2dHash_DirectionHasher.pxx>
+
+//! OCCT-style hasher for gp_Ax22d (2D coordinate system).
+//! Used for geometry deduplication.
+struct Geom2dHash_AxisPlacement
+{
+  // Hashes a 2D axis placement by its location, X direction, and Y direction.
+  std::size_t operator()(const gp_Ax22d& theAxisPlacement) const noexcept
+  {
+    const Geom2dHash_PointHasher     aPointHasher;
+    const Geom2dHash_DirectionHasher aDirHasher;
+
+    const std::size_t aHashes[3] = {aPointHasher(theAxisPlacement.Location()),
+                                    aDirHasher(theAxisPlacement.XDirection()),
+                                    aDirHasher(theAxisPlacement.YDirection())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 2D axis placements.
+  bool operator()(const gp_Ax22d& theAxisPlacement1,
+                  const gp_Ax22d& theAxisPlacement2) const noexcept
+  {
+    const Geom2dHash_PointHasher     aPointHasher;
+    const Geom2dHash_DirectionHasher aDirHasher;
+
+    return aPointHasher(theAxisPlacement1.Location(), theAxisPlacement2.Location())
+           && aDirHasher(theAxisPlacement1.XDirection(), theAxisPlacement2.XDirection())
+           && aDirHasher(theAxisPlacement1.YDirection(), theAxisPlacement2.YDirection());
+  }
+};
+
+#endif // _Geom2dHash_AxisPlacement_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BSplineCurveHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BSplineCurveHasher.pxx
new file mode 100644 (file)
index 0000000..9dea2eb
--- /dev/null
@@ -0,0 +1,98 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_BSplineCurveHasher_HeaderFile
+#define _Geom2dHash_BSplineCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_BSplineCurve.hxx>
+#include <Geom2dHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_BSplineCurve (2D B-spline curve).
+//! Used for geometry deduplication.
+//! Hashes only metadata (degree, pole count, knot count, rationality) for efficiency.
+struct Geom2dHash_BSplineCurveHasher
+{
+  // Hashes the B-spline curve metadata only.
+  std::size_t operator()(const Handle(Geom2d_BSplineCurve)& theCurve) const noexcept
+  {
+    const std::size_t aHashes[4] = {opencascade::hash(theCurve->Degree()),
+                                    opencascade::hash(theCurve->NbPoles()),
+                                    opencascade::hash(theCurve->NbKnots()),
+                                    opencascade::hash(static_cast<int>(theCurve->IsRational()))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two B-spline curves by full geometric data.
+  bool operator()(const Handle(Geom2d_BSplineCurve)& theCurve1,
+                  const Handle(Geom2d_BSplineCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theCurve1->Degree() != theCurve2->Degree())
+    {
+      return false;
+    }
+
+    // Compare knot counts
+    if (theCurve1->NbKnots() != theCurve2->NbKnots())
+    {
+      return false;
+    }
+
+    // Compare knots and multiplicities
+    for (int i = 1; i <= theCurve1->NbKnots(); ++i)
+    {
+      if (std::abs(theCurve1->Knot(i) - theCurve2->Knot(i)) > aTolerance
+          || theCurve1->Multiplicity(i) != theCurve2->Multiplicity(i))
+      {
+        return false;
+      }
+    }
+
+    // Compare rationality
+    if (theCurve1->IsRational() != theCurve2->IsRational())
+    {
+      return false;
+    }
+
+    const Geom2dHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+    {
+      if (!aPointHasher(theCurve1->Pole(i), theCurve2->Pole(i)))
+      {
+        return false;
+      }
+    }
+
+    // Compare weights if rational
+    if (theCurve1->IsRational())
+    {
+      for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+      {
+        if (std::abs(theCurve1->Weight(i) - theCurve2->Weight(i)) > aTolerance)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _Geom2dHash_BSplineCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BezierCurveHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_BezierCurveHasher.pxx
new file mode 100644 (file)
index 0000000..3e46201
--- /dev/null
@@ -0,0 +1,81 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_BezierCurveHasher_HeaderFile
+#define _Geom2dHash_BezierCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_BezierCurve.hxx>
+#include <Geom2dHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_BezierCurve (2D Bezier curve).
+//! Used for geometry deduplication.
+//! Hashes only metadata (degree, pole count, rationality) for efficiency.
+struct Geom2dHash_BezierCurveHasher
+{
+  // Hashes the Bezier curve metadata only.
+  std::size_t operator()(const Handle(Geom2d_BezierCurve)& theCurve) const noexcept
+  {
+    const std::size_t aHashes[3] = {opencascade::hash(theCurve->Degree()),
+                                    opencascade::hash(theCurve->NbPoles()),
+                                    opencascade::hash(static_cast<int>(theCurve->IsRational()))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two Bezier curves by full geometric data.
+  bool operator()(const Handle(Geom2d_BezierCurve)& theCurve1,
+                  const Handle(Geom2d_BezierCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theCurve1->Degree() != theCurve2->Degree())
+    {
+      return false;
+    }
+
+    // Compare rationality
+    if (theCurve1->IsRational() != theCurve2->IsRational())
+    {
+      return false;
+    }
+
+    const Geom2dHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+    {
+      if (!aPointHasher(theCurve1->Pole(i), theCurve2->Pole(i)))
+      {
+        return false;
+      }
+    }
+
+    // Compare weights if rational
+    if (theCurve1->IsRational())
+    {
+      for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+      {
+        if (std::abs(theCurve1->Weight(i) - theCurve2->Weight(i)) > aTolerance)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _Geom2dHash_BezierCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CircleHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CircleHasher.pxx
new file mode 100644 (file)
index 0000000..c8f8665
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_CircleHasher_HeaderFile
+#define _Geom2dHash_CircleHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Circle.hxx>
+#include <Geom2dHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_Circle (2D circle).
+//! Used for geometry deduplication.
+struct Geom2dHash_CircleHasher
+{
+  // Hashes the circle by its position and radius.
+  std::size_t operator()(const Handle(Geom2d_Circle)& theCircle) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_AxisPlacement anAxisHasher;
+    const std::size_t              aHashes[2] = {
+      anAxisHasher(theCircle->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCircle->Radius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two circles by their positions and radii.
+  bool operator()(const Handle(Geom2d_Circle)& theCircle1,
+                  const Handle(Geom2d_Circle)& theCircle2) const noexcept
+  {
+    constexpr double               aTolerance = 1e-12;
+    const Geom2dHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theCircle1->Position(), theCircle2->Position())
+           && std::abs(theCircle1->Radius() - theCircle2->Radius()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_CircleHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.cxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.cxx
new file mode 100644 (file)
index 0000000..8b29a76
--- /dev/null
@@ -0,0 +1,152 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <Geom2dHash_CurveHasher.hxx>
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Curve.hxx>
+#include <Geom2d_Line.hxx>
+#include <Geom2d_Circle.hxx>
+#include <Geom2d_Ellipse.hxx>
+#include <Geom2d_Hyperbola.hxx>
+#include <Geom2d_Parabola.hxx>
+#include <Geom2d_BezierCurve.hxx>
+#include <Geom2d_BSplineCurve.hxx>
+#include <Geom2d_TrimmedCurve.hxx>
+#include <Geom2d_OffsetCurve.hxx>
+
+#include <Geom2dHash_LineHasher.pxx>
+#include <Geom2dHash_CircleHasher.pxx>
+#include <Geom2dHash_EllipseHasher.pxx>
+#include <Geom2dHash_HyperbolaHasher.pxx>
+#include <Geom2dHash_ParabolaHasher.pxx>
+#include <Geom2dHash_BezierCurveHasher.pxx>
+#include <Geom2dHash_BSplineCurveHasher.pxx>
+#include <Geom2dHash_TrimmedCurveHasher.pxx>
+#include <Geom2dHash_OffsetCurveHasher.pxx>
+
+//=================================================================================================
+
+std::size_t Geom2dHash_CurveHasher::operator()(const Handle(Geom2d_Curve)& theCurve) const noexcept
+{
+  if (theCurve.IsNull())
+  {
+    return 0;
+  }
+
+  // Dispatch based on actual curve type
+  if (Handle(Geom2d_Line) aLine = Handle(Geom2d_Line)::DownCast(theCurve))
+  {
+    return Geom2dHash_LineHasher{}(aLine);
+  }
+  if (Handle(Geom2d_Circle) aCircle = Handle(Geom2d_Circle)::DownCast(theCurve))
+  {
+    return Geom2dHash_CircleHasher{}(aCircle);
+  }
+  if (Handle(Geom2d_Ellipse) anEllipse = Handle(Geom2d_Ellipse)::DownCast(theCurve))
+  {
+    return Geom2dHash_EllipseHasher{}(anEllipse);
+  }
+  if (Handle(Geom2d_Hyperbola) aHyperbola = Handle(Geom2d_Hyperbola)::DownCast(theCurve))
+  {
+    return Geom2dHash_HyperbolaHasher{}(aHyperbola);
+  }
+  if (Handle(Geom2d_Parabola) aParabola = Handle(Geom2d_Parabola)::DownCast(theCurve))
+  {
+    return Geom2dHash_ParabolaHasher{}(aParabola);
+  }
+  if (Handle(Geom2d_BezierCurve) aBezier = Handle(Geom2d_BezierCurve)::DownCast(theCurve))
+  {
+    return Geom2dHash_BezierCurveHasher{}(aBezier);
+  }
+  if (Handle(Geom2d_BSplineCurve) aBSpline = Handle(Geom2d_BSplineCurve)::DownCast(theCurve))
+  {
+    return Geom2dHash_BSplineCurveHasher{}(aBSpline);
+  }
+  if (Handle(Geom2d_TrimmedCurve) aTrimmed = Handle(Geom2d_TrimmedCurve)::DownCast(theCurve))
+  {
+    return Geom2dHash_TrimmedCurveHasher{}(aTrimmed);
+  }
+  if (Handle(Geom2d_OffsetCurve) anOffset = Handle(Geom2d_OffsetCurve)::DownCast(theCurve))
+  {
+    return Geom2dHash_OffsetCurveHasher{}(anOffset);
+  }
+
+  // Unknown curve type - hash the type name
+  return std::hash<std::string>{}(theCurve->DynamicType()->Name());
+}
+
+//=================================================================================================
+
+bool Geom2dHash_CurveHasher::operator()(const Handle(Geom2d_Curve)& theCurve1,
+                                        const Handle(Geom2d_Curve)& theCurve2) const noexcept
+{
+  if (theCurve1.IsNull() || theCurve2.IsNull())
+  {
+    return theCurve1.IsNull() && theCurve2.IsNull();
+  }
+
+  if (theCurve1 == theCurve2)
+  {
+    return true;
+  }
+
+  // Must be same type
+  if (theCurve1->DynamicType() != theCurve2->DynamicType())
+  {
+    return false;
+  }
+
+  // Dispatch based on actual curve type
+  if (Handle(Geom2d_Line) aLine1 = Handle(Geom2d_Line)::DownCast(theCurve1))
+  {
+    return Geom2dHash_LineHasher{}(aLine1, Handle(Geom2d_Line)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_Circle) aCircle1 = Handle(Geom2d_Circle)::DownCast(theCurve1))
+  {
+    return Geom2dHash_CircleHasher{}(aCircle1, Handle(Geom2d_Circle)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_Ellipse) anEllipse1 = Handle(Geom2d_Ellipse)::DownCast(theCurve1))
+  {
+    return Geom2dHash_EllipseHasher{}(anEllipse1, Handle(Geom2d_Ellipse)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_Hyperbola) aHyp1 = Handle(Geom2d_Hyperbola)::DownCast(theCurve1))
+  {
+    return Geom2dHash_HyperbolaHasher{}(aHyp1, Handle(Geom2d_Hyperbola)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_Parabola) aPar1 = Handle(Geom2d_Parabola)::DownCast(theCurve1))
+  {
+    return Geom2dHash_ParabolaHasher{}(aPar1, Handle(Geom2d_Parabola)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_BezierCurve) aBez1 = Handle(Geom2d_BezierCurve)::DownCast(theCurve1))
+  {
+    return Geom2dHash_BezierCurveHasher{}(aBez1, Handle(Geom2d_BezierCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_BSplineCurve) aBSpl1 = Handle(Geom2d_BSplineCurve)::DownCast(theCurve1))
+  {
+    return Geom2dHash_BSplineCurveHasher{}(aBSpl1,
+                                           Handle(Geom2d_BSplineCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_TrimmedCurve) aTrim1 = Handle(Geom2d_TrimmedCurve)::DownCast(theCurve1))
+  {
+    return Geom2dHash_TrimmedCurveHasher{}(aTrim1,
+                                           Handle(Geom2d_TrimmedCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom2d_OffsetCurve) aOff1 = Handle(Geom2d_OffsetCurve)::DownCast(theCurve1))
+  {
+    return Geom2dHash_OffsetCurveHasher{}(aOff1, Handle(Geom2d_OffsetCurve)::DownCast(theCurve2));
+  }
+
+  // Unknown curve type - compare by pointer
+  return theCurve1.get() == theCurve2.get();
+}
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.hxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_CurveHasher.hxx
new file mode 100644 (file)
index 0000000..fe35e77
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_CurveHasher_HeaderFile
+#define _Geom2dHash_CurveHasher_HeaderFile
+
+#include <Standard_Handle.hxx>
+#include <cstddef>
+
+class Geom2d_Curve;
+
+//! Polymorphic hasher for Geom2d_Curve using RTTI dispatch.
+//! Used for geometry deduplication.
+struct Geom2dHash_CurveHasher
+{
+  // Hashes any Geom2d_Curve by dispatching to the appropriate specific hasher.
+  Standard_EXPORT std::size_t operator()(const Handle(Geom2d_Curve)& theCurve) const noexcept;
+
+  // Compares two curves using polymorphic dispatch.
+  Standard_EXPORT bool operator()(const Handle(Geom2d_Curve)& theCurve1,
+                                  const Handle(Geom2d_Curve)& theCurve2) const noexcept;
+};
+
+#endif // _Geom2dHash_CurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_DirectionHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_DirectionHasher.pxx
new file mode 100644 (file)
index 0000000..86e73a7
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_DirectionHasher_HeaderFile
+#define _Geom2dHash_DirectionHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Dir2d.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for gp_Dir2d (2D directions).
+//! Used for geometry deduplication.
+struct Geom2dHash_DirectionHasher
+{
+  // Hashes the 2D direction by its XY components.
+  std::size_t operator()(const gp_Dir2d& theDirection) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    // Round each component to tolerance precision before hashing
+    const std::size_t aHashes[2] = {
+      opencascade::hash(static_cast<int64_t>(std::round(theDirection.X() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theDirection.Y() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 2D directions with fixed tolerance.
+  bool operator()(const gp_Dir2d& theDirection1, const gp_Dir2d& theDirection2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    return std::abs(theDirection1.X() - theDirection2.X()) <= aTolerance
+           && std::abs(theDirection1.Y() - theDirection2.Y()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_DirectionHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_EllipseHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_EllipseHasher.pxx
new file mode 100644 (file)
index 0000000..398bb5b
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_EllipseHasher_HeaderFile
+#define _Geom2dHash_EllipseHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Ellipse.hxx>
+#include <Geom2dHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_Ellipse (2D ellipse).
+//! Used for geometry deduplication.
+struct Geom2dHash_EllipseHasher
+{
+  // Hashes the ellipse by its position, major radius, and minor radius.
+  std::size_t operator()(const Handle(Geom2d_Ellipse)& theEllipse) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_AxisPlacement anAxisHasher;
+    const std::size_t              aHashes[3] = {
+      anAxisHasher(theEllipse->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theEllipse->MajorRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theEllipse->MinorRadius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two ellipses by their positions and radii.
+  bool operator()(const Handle(Geom2d_Ellipse)& theEllipse1,
+                  const Handle(Geom2d_Ellipse)& theEllipse2) const noexcept
+  {
+    constexpr double               aTolerance = 1e-12;
+    const Geom2dHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theEllipse1->Position(), theEllipse2->Position())
+           && std::abs(theEllipse1->MajorRadius() - theEllipse2->MajorRadius()) <= aTolerance
+           && std::abs(theEllipse1->MinorRadius() - theEllipse2->MinorRadius()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_EllipseHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_HyperbolaHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_HyperbolaHasher.pxx
new file mode 100644 (file)
index 0000000..c8de9ec
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_HyperbolaHasher_HeaderFile
+#define _Geom2dHash_HyperbolaHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Hyperbola.hxx>
+#include <Geom2dHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_Hyperbola (2D hyperbola).
+//! Used for geometry deduplication.
+struct Geom2dHash_HyperbolaHasher
+{
+  // Hashes the hyperbola by its position, major radius, and minor radius.
+  std::size_t operator()(const Handle(Geom2d_Hyperbola)& theHyperbola) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_AxisPlacement anAxisHasher;
+    const std::size_t              aHashes[3] = {
+      anAxisHasher(theHyperbola->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theHyperbola->MajorRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theHyperbola->MinorRadius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two hyperbolas by their positions and radii.
+  bool operator()(const Handle(Geom2d_Hyperbola)& theHyperbola1,
+                  const Handle(Geom2d_Hyperbola)& theHyperbola2) const noexcept
+  {
+    constexpr double               aTolerance = 1e-12;
+    const Geom2dHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theHyperbola1->Position(), theHyperbola2->Position())
+           && std::abs(theHyperbola1->MajorRadius() - theHyperbola2->MajorRadius()) <= aTolerance
+           && std::abs(theHyperbola1->MinorRadius() - theHyperbola2->MinorRadius()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_HyperbolaHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_LineHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_LineHasher.pxx
new file mode 100644 (file)
index 0000000..873a521
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_LineHasher_HeaderFile
+#define _Geom2dHash_LineHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Line.hxx>
+#include <Geom2dHash_PointHasher.pxx>
+#include <Geom2dHash_DirectionHasher.pxx>
+
+//! OCCT-style hasher for Geom2d_Line (2D line).
+//! Used for geometry deduplication.
+struct Geom2dHash_LineHasher
+{
+  // Hashes the line by its location and direction.
+  std::size_t operator()(const Handle(Geom2d_Line)& theLine) const noexcept
+  {
+    const Geom2dHash_PointHasher     aPointHasher;
+    const Geom2dHash_DirectionHasher aDirHasher;
+
+    const std::size_t aHashes[2] = {aPointHasher(theLine->Position().Location()),
+                                    aDirHasher(theLine->Position().Direction())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two lines by their positions.
+  bool operator()(const Handle(Geom2d_Line)& theLine1,
+                  const Handle(Geom2d_Line)& theLine2) const noexcept
+  {
+    const Geom2dHash_PointHasher     aPointHasher;
+    const Geom2dHash_DirectionHasher aDirHasher;
+
+    return aPointHasher(theLine1->Position().Location(), theLine2->Position().Location())
+           && aDirHasher(theLine1->Position().Direction(), theLine2->Position().Direction());
+  }
+};
+
+#endif // _Geom2dHash_LineHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_OffsetCurveHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_OffsetCurveHasher.pxx
new file mode 100644 (file)
index 0000000..e10a5ac
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_OffsetCurveHasher_HeaderFile
+#define _Geom2dHash_OffsetCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_OffsetCurve.hxx>
+#include <Geom2dHash_CurveHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_OffsetCurve (2D offset curve).
+//! Used for geometry deduplication.
+struct Geom2dHash_OffsetCurveHasher
+{
+  // Hashes the offset curve by its offset distance and basis curve.
+  std::size_t operator()(const Handle(Geom2d_OffsetCurve)& theCurve) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_CurveHasher aCurveHasher;
+    const std::size_t            aHashes[2] = {
+      aCurveHasher(theCurve->BasisCurve()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->Offset() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two offset curves.
+  bool operator()(const Handle(Geom2d_OffsetCurve)& theCurve1,
+                  const Handle(Geom2d_OffsetCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const Geom2dHash_CurveHasher aCurveHasher;
+
+    return aCurveHasher(theCurve1->BasisCurve(), theCurve2->BasisCurve())
+           && std::abs(theCurve1->Offset() - theCurve2->Offset()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_OffsetCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_ParabolaHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_ParabolaHasher.pxx
new file mode 100644 (file)
index 0000000..3fea42c
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_ParabolaHasher_HeaderFile
+#define _Geom2dHash_ParabolaHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_Parabola.hxx>
+#include <Geom2dHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_Parabola (2D parabola).
+//! Used for geometry deduplication.
+struct Geom2dHash_ParabolaHasher
+{
+  // Hashes the parabola by its position and focal length.
+  std::size_t operator()(const Handle(Geom2d_Parabola)& theParabola) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_AxisPlacement anAxisHasher;
+    const std::size_t              aHashes[2] = {
+      anAxisHasher(theParabola->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theParabola->Focal() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two parabolas by their positions and focal lengths.
+  bool operator()(const Handle(Geom2d_Parabola)& theParabola1,
+                  const Handle(Geom2d_Parabola)& theParabola2) const noexcept
+  {
+    constexpr double               aTolerance = 1e-12;
+    const Geom2dHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theParabola1->Position(), theParabola2->Position())
+           && std::abs(theParabola1->Focal() - theParabola2->Focal()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_ParabolaHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_PointHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_PointHasher.pxx
new file mode 100644 (file)
index 0000000..12b31c1
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_PointHasher_HeaderFile
+#define _Geom2dHash_PointHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Pnt2d.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for gp_Pnt2d (2D points).
+//! Used for geometry deduplication.
+struct Geom2dHash_PointHasher
+{
+  // Hashes the 2D point by its XY coordinates.
+  std::size_t operator()(const gp_Pnt2d& thePoint) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    // Round each coordinate to tolerance precision before hashing
+    const std::size_t aHashes[2] = {
+      opencascade::hash(static_cast<int64_t>(std::round(thePoint.X() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(thePoint.Y() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 2D points with fixed tolerance.
+  bool operator()(const gp_Pnt2d& thePoint1, const gp_Pnt2d& thePoint2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    return std::abs(thePoint1.X() - thePoint2.X()) <= aTolerance
+           && std::abs(thePoint1.Y() - thePoint2.Y()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_PointHasher_HeaderFile
diff --git a/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_TrimmedCurveHasher.pxx b/src/ModelingData/TKG2d/Geom2dHash/Geom2dHash_TrimmedCurveHasher.pxx
new file mode 100644 (file)
index 0000000..3da1da4
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _Geom2dHash_TrimmedCurveHasher_HeaderFile
+#define _Geom2dHash_TrimmedCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom2d_TrimmedCurve.hxx>
+#include <Geom2dHash_CurveHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom2d_TrimmedCurve (2D trimmed curve).
+//! Used for geometry deduplication.
+struct Geom2dHash_TrimmedCurveHasher
+{
+  // Hashes the trimmed curve by its parameters and basis curve.
+  std::size_t operator()(const Handle(Geom2d_TrimmedCurve)& theCurve) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const Geom2dHash_CurveHasher aCurveHasher;
+    const std::size_t            aHashes[3] = {
+      aCurveHasher(theCurve->BasisCurve()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->FirstParameter() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->LastParameter() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two trimmed curves.
+  bool operator()(const Handle(Geom2d_TrimmedCurve)& theCurve1,
+                  const Handle(Geom2d_TrimmedCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const Geom2dHash_CurveHasher aCurveHasher;
+
+    return aCurveHasher(theCurve1->BasisCurve(), theCurve2->BasisCurve())
+           && std::abs(theCurve1->FirstParameter() - theCurve2->FirstParameter()) <= aTolerance
+           && std::abs(theCurve1->LastParameter() - theCurve2->LastParameter()) <= aTolerance;
+  }
+};
+
+#endif // _Geom2dHash_TrimmedCurveHasher_HeaderFile
index 3b08d250586f3704d9c65a8569a503967f128d6a..53148bd634616076dfa0309f0de7e9f305e8e5ff 100644 (file)
@@ -7,4 +7,5 @@ set(OCCT_TKG2d_LIST_OF_PACKAGES
   Geom2dLProp
   Geom2dAdaptor
   Geom2dEvaluator
+  Geom2dHash
 )
index 679da3f86646539d074ea929da86a1199c7db639..3e7d05db7c0759e25426635da36d9ea0a36c0eb2 100644 (file)
@@ -10,4 +10,6 @@ set(OCCT_TKG3d_GTests_FILES
   Geom_OffsetSurface_Test.cxx
   GeomAPI_ExtremaCurveCurve_Test.cxx
   GeomAPI_Interpolate_Test.cxx
+  GeomHash_CurveHasher_Test.cxx
+  GeomHash_SurfaceHasher_Test.cxx
 )
diff --git a/src/ModelingData/TKG3d/GTests/GeomHash_CurveHasher_Test.cxx b/src/ModelingData/TKG3d/GTests/GeomHash_CurveHasher_Test.cxx
new file mode 100644 (file)
index 0000000..6a80547
--- /dev/null
@@ -0,0 +1,690 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <gtest/gtest.h>
+
+#include <GeomHash_CurveHasher.hxx>
+#include <Geom_Line.hxx>
+#include <Geom_Circle.hxx>
+#include <Geom_Ellipse.hxx>
+#include <Geom_Hyperbola.hxx>
+#include <Geom_Parabola.hxx>
+#include <Geom_BezierCurve.hxx>
+#include <Geom_BSplineCurve.hxx>
+#include <Geom_TrimmedCurve.hxx>
+#include <Geom_OffsetCurve.hxx>
+#include <gp_Ax1.hxx>
+#include <gp_Ax2.hxx>
+#include <gp_Pnt.hxx>
+#include <gp_Dir.hxx>
+#include <gp_Vec.hxx>
+#include <TColgp_Array1OfPnt.hxx>
+#include <TColStd_Array1OfReal.hxx>
+#include <TColStd_Array1OfInteger.hxx>
+
+class GeomHash_CurveHasherTest : public ::testing::Test
+{
+protected:
+  GeomHash_CurveHasher myHasher;
+};
+
+// ============================================================================
+// Line Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Line_CopiedLines_SameHash)
+{
+  gp_Pnt            aLoc(1.0, 2.0, 3.0);
+  gp_Dir            aDir(1.0, 0.0, 0.0);
+  Handle(Geom_Line) aLine1 = new Geom_Line(aLoc, aDir);
+  Handle(Geom_Line) aLine2 = Handle(Geom_Line)::DownCast(aLine1->Copy());
+
+  EXPECT_EQ(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_TRUE(myHasher(aLine1, aLine2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Line_DifferentLines_DifferentHash)
+{
+  Handle(Geom_Line) aLine1 = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_Line) aLine2 = new Geom_Line(gp_Pnt(1.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+// ============================================================================
+// Circle Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Circle_CopiedCircles_SameHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 5.0);
+  Handle(Geom_Circle) aCircle2 = Handle(Geom_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Circle_DifferentRadius_DifferentHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 5.0);
+  Handle(Geom_Circle) aCircle2 = new Geom_Circle(anAxis, 10.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+// ============================================================================
+// Ellipse Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Ellipse_CopiedEllipses_SameHash)
+{
+  gp_Ax2               anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Ellipse) anEllipse1 = new Geom_Ellipse(anAxis, 10.0, 5.0);
+  Handle(Geom_Ellipse) anEllipse2 = Handle(Geom_Ellipse)::DownCast(anEllipse1->Copy());
+
+  EXPECT_EQ(myHasher(anEllipse1), myHasher(anEllipse2));
+  EXPECT_TRUE(myHasher(anEllipse1, anEllipse2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Ellipse_DifferentRadii_DifferentHash)
+{
+  gp_Ax2               anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Ellipse) anEllipse1 = new Geom_Ellipse(anAxis, 10.0, 5.0);
+  Handle(Geom_Ellipse) anEllipse2 = new Geom_Ellipse(anAxis, 10.0, 7.0);
+
+  EXPECT_NE(myHasher(anEllipse1), myHasher(anEllipse2));
+  EXPECT_FALSE(myHasher(anEllipse1, anEllipse2));
+}
+
+// ============================================================================
+// Hyperbola Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Hyperbola_CopiedHyperbolas_SameHash)
+{
+  gp_Ax2                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Hyperbola) aHyp1 = new Geom_Hyperbola(anAxis, 5.0, 3.0);
+  Handle(Geom_Hyperbola) aHyp2 = Handle(Geom_Hyperbola)::DownCast(aHyp1->Copy());
+
+  EXPECT_EQ(myHasher(aHyp1), myHasher(aHyp2));
+  EXPECT_TRUE(myHasher(aHyp1, aHyp2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Hyperbola_DifferentRadii_DifferentHash)
+{
+  gp_Ax2                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Hyperbola) aHyp1 = new Geom_Hyperbola(anAxis, 5.0, 3.0);
+  Handle(Geom_Hyperbola) aHyp2 = new Geom_Hyperbola(anAxis, 5.0, 4.0);
+
+  EXPECT_NE(myHasher(aHyp1), myHasher(aHyp2));
+  EXPECT_FALSE(myHasher(aHyp1, aHyp2));
+}
+
+// ============================================================================
+// Parabola Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Parabola_CopiedParabolas_SameHash)
+{
+  gp_Ax2                anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Parabola) aPar1 = new Geom_Parabola(anAxis, 2.0);
+  Handle(Geom_Parabola) aPar2 = Handle(Geom_Parabola)::DownCast(aPar1->Copy());
+
+  EXPECT_EQ(myHasher(aPar1), myHasher(aPar2));
+  EXPECT_TRUE(myHasher(aPar1, aPar2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Parabola_DifferentFocal_DifferentHash)
+{
+  gp_Ax2                anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Parabola) aPar1 = new Geom_Parabola(anAxis, 2.0);
+  Handle(Geom_Parabola) aPar2 = new Geom_Parabola(anAxis, 3.0);
+
+  EXPECT_NE(myHasher(aPar1), myHasher(aPar2));
+  EXPECT_FALSE(myHasher(aPar1, aPar2));
+}
+
+// ============================================================================
+// BezierCurve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BezierCurve_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 2.0, 0.0);
+  aPoles(3) = gp_Pnt(3.0, 2.0, 0.0);
+  aPoles(4) = gp_Pnt(4.0, 0.0, 0.0);
+
+  Handle(Geom_BezierCurve) aCurve1 = new Geom_BezierCurve(aPoles);
+  Handle(Geom_BezierCurve) aCurve2 = Handle(Geom_BezierCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, BezierCurve_DifferentPoles_DifferentComparison)
+{
+  TColgp_Array1OfPnt aPoles1(1, 3);
+  aPoles1(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles1(2) = gp_Pnt(1.0, 2.0, 0.0);
+  aPoles1(3) = gp_Pnt(2.0, 0.0, 0.0);
+
+  TColgp_Array1OfPnt aPoles2(1, 3);
+  aPoles2(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles2(2) = gp_Pnt(1.0, 3.0, 0.0); // Different
+  aPoles2(3) = gp_Pnt(2.0, 0.0, 0.0);
+
+  Handle(Geom_BezierCurve) aCurve1 = new Geom_BezierCurve(aPoles1);
+  Handle(Geom_BezierCurve) aCurve2 = new Geom_BezierCurve(aPoles2);
+
+  // Hash may be same (metadata only), but comparison should differ
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// BSplineCurve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 1.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 3);
+  Handle(Geom_BSplineCurve) aCurve2 = Handle(Geom_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// TrimmedCurve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, TrimmedCurve_CopiedCurves_SameHash)
+{
+  Handle(Geom_Line)         aLine     = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_TrimmedCurve) aTrimmed1 = new Geom_TrimmedCurve(aLine, 0.0, 10.0);
+  Handle(Geom_TrimmedCurve) aTrimmed2 = Handle(Geom_TrimmedCurve)::DownCast(aTrimmed1->Copy());
+
+  EXPECT_EQ(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_TRUE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, TrimmedCurve_DifferentBounds_DifferentHash)
+{
+  Handle(Geom_Line)         aLine     = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_TrimmedCurve) aTrimmed1 = new Geom_TrimmedCurve(aLine, 0.0, 10.0);
+  Handle(Geom_TrimmedCurve) aTrimmed2 = new Geom_TrimmedCurve(aLine, 0.0, 20.0);
+
+  EXPECT_NE(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_FALSE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+// ============================================================================
+// OffsetCurve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, OffsetCurve_CopiedCurves_SameHash)
+{
+  Handle(Geom_Line)        aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  gp_Dir                   aRefDir(0.0, 0.0, 1.0);
+  Handle(Geom_OffsetCurve) anOffset1 = new Geom_OffsetCurve(aLine, 5.0, aRefDir);
+  Handle(Geom_OffsetCurve) anOffset2 = Handle(Geom_OffsetCurve)::DownCast(anOffset1->Copy());
+
+  EXPECT_EQ(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_TRUE(myHasher(anOffset1, anOffset2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, OffsetCurve_DifferentOffset_DifferentHash)
+{
+  Handle(Geom_Line)        aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  gp_Dir                   aRefDir(0.0, 0.0, 1.0);
+  Handle(Geom_OffsetCurve) anOffset1 = new Geom_OffsetCurve(aLine, 5.0, aRefDir);
+  Handle(Geom_OffsetCurve) anOffset2 = new Geom_OffsetCurve(aLine, 10.0, aRefDir);
+
+  EXPECT_NE(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_FALSE(myHasher(anOffset1, anOffset2));
+}
+
+// ============================================================================
+// Cross-type Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, DifferentTypes_DifferentComparison)
+{
+  Handle(Geom_Line)   aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle = new Geom_Circle(anAxis, 5.0);
+
+  EXPECT_FALSE(myHasher(aLine, aCircle));
+}
+
+TEST_F(GeomHash_CurveHasherTest, NullCurves_HandledCorrectly)
+{
+  Handle(Geom_Curve) aNullCurve;
+  Handle(Geom_Line)  aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+
+  EXPECT_EQ(myHasher(aNullCurve), 0u);
+  EXPECT_TRUE(myHasher(aNullCurve, aNullCurve));
+  EXPECT_FALSE(myHasher(aLine, aNullCurve));
+}
+
+TEST_F(GeomHash_CurveHasherTest, SameObject_Equal)
+{
+  Handle(Geom_Line) aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  EXPECT_TRUE(myHasher(aLine, aLine));
+}
+
+// ============================================================================
+// Weighted BSpline Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_Weighted_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 1.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aWeights(1, 4);
+  aWeights(1) = 1.0;
+  aWeights(2) = 2.0;
+  aWeights(3) = 2.0;
+  aWeights(4) = 1.0;
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aWeights, aKnots, aMults, 3);
+  Handle(Geom_BSplineCurve) aCurve2 = Handle(Geom_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_DifferentWeights_DifferentComparison)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 1.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aWeights1(1, 4);
+  aWeights1(1) = 1.0;
+  aWeights1(2) = 2.0;
+  aWeights1(3) = 2.0;
+  aWeights1(4) = 1.0;
+
+  TColStd_Array1OfReal aWeights2(1, 4);
+  aWeights2(1) = 1.0;
+  aWeights2(2) = 3.0; // Different
+  aWeights2(3) = 2.0;
+  aWeights2(4) = 1.0;
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aWeights1, aKnots, aMults, 3);
+  Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles, aWeights2, aKnots, aMults, 3);
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Weighted Bezier Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BezierCurve_Weighted_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt aPoles(1, 3);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 2.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aWeights(1, 3);
+  aWeights(1) = 1.0;
+  aWeights(2) = 2.0;
+  aWeights(3) = 1.0;
+
+  Handle(Geom_BezierCurve) aCurve1 = new Geom_BezierCurve(aPoles, aWeights);
+  Handle(Geom_BezierCurve) aCurve2 = Handle(Geom_BezierCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Axis Orientation Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Circle_DifferentAxisOrientation_DifferentHash)
+{
+  gp_Ax2              anAxis1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax2              anAxis2(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis1, 5.0);
+  Handle(Geom_Circle) aCircle2 = new Geom_Circle(anAxis2, 5.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Line_DifferentDirection_DifferentHash)
+{
+  Handle(Geom_Line) aLine1 = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_Line) aLine2 = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+// ============================================================================
+// BSpline with Different Knots Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_DifferentKnots_DifferentComparison)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 1.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aKnots1(1, 2);
+  aKnots1(1) = 0.0;
+  aKnots1(2) = 1.0;
+
+  TColStd_Array1OfReal aKnots2(1, 2);
+  aKnots2(1) = 0.0;
+  aKnots2(2) = 2.0; // Different
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 4;
+  aMults(2) = 4;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots1, aMults, 3);
+  Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles, aKnots2, aMults, 3);
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Periodic BSpline Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_QuadraticBezierEquivalent_CopiedCurves_SameHash)
+{
+  // Simple quadratic B-spline (Bezier-like, single span)
+  TColgp_Array1OfPnt aPoles(1, 3);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 2.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 3;
+  aMults(2) = 3;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 2);
+  Handle(Geom_BSplineCurve) aCurve2 = Handle(Geom_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Reversed Curve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Line_Reversed_DifferentComparison)
+{
+  Handle(Geom_Line) aLine1 = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_Line) aLine2 = Handle(Geom_Line)::DownCast(aLine1->Reversed());
+
+  // Reversed line has opposite direction
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, BezierCurve_Reversed_DifferentComparison)
+{
+  TColgp_Array1OfPnt aPoles(1, 3);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 2.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 0.0, 0.0);
+
+  Handle(Geom_BezierCurve) aCurve1 = new Geom_BezierCurve(aPoles);
+  Handle(Geom_BezierCurve) aCurve2 = Handle(Geom_BezierCurve)::DownCast(aCurve1->Reversed());
+
+  // Reversed curve has poles in different order
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Transformed Curve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Circle_Translated_DifferentHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 5.0);
+  Handle(Geom_Circle) aCircle2 = Handle(Geom_Circle)::DownCast(aCircle1->Copy());
+  aCircle2->Translate(gp_Vec(1.0, 0.0, 0.0));
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Circle_Scaled_DifferentHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 5.0);
+  Handle(Geom_Circle) aCircle2 = Handle(Geom_Circle)::DownCast(aCircle1->Copy());
+  aCircle2->Scale(gp_Pnt(0.0, 0.0, 0.0), 2.0);
+
+  EXPECT_NE(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_FALSE(myHasher(aCircle1, aCircle2));
+}
+
+// ============================================================================
+// Higher Degree BSpline Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_HigherDegree_CopiedCurves_SameHash)
+{
+  TColgp_Array1OfPnt aPoles(1, 6);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 0.5, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 1.5, 0.0);
+  aPoles(5) = gp_Pnt(4.0, 0.0, 0.0);
+  aPoles(6) = gp_Pnt(5.0, 1.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults(1, 2);
+  aMults(1) = 6;
+  aMults(2) = 6;
+
+  // Degree 5 curve
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 5);
+  Handle(Geom_BSplineCurve) aCurve2 = Handle(Geom_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_DifferentDegree_DifferentComparison)
+{
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 1.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 0.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 2);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aMults1(1, 2);
+  aMults1(1) = 4;
+  aMults1(2) = 4;
+
+  TColStd_Array1OfInteger aMults2(1, 2);
+  aMults2(1) = 3;
+  aMults2(2) = 3;
+
+  // Different degrees: 3 vs 2
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults1, 3);
+
+  TColgp_Array1OfPnt aPoles2(1, 3);
+  aPoles2(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles2(2) = gp_Pnt(1.5, 1.0, 0.0);
+  aPoles2(3) = gp_Pnt(3.0, 0.0, 0.0);
+
+  Handle(Geom_BSplineCurve) aCurve2 = new Geom_BSplineCurve(aPoles2, aKnots, aMults2, 2);
+
+  EXPECT_FALSE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Multiple Knot Spans Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, BSplineCurve_LinearMultipleSpans_CopiedCurves_SameHash)
+{
+  // Linear B-spline with multiple knots (piecewise linear)
+  TColgp_Array1OfPnt aPoles(1, 4);
+  aPoles(1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(2) = gp_Pnt(1.0, 1.0, 0.0);
+  aPoles(3) = gp_Pnt(2.0, 0.0, 0.0);
+  aPoles(4) = gp_Pnt(3.0, 1.0, 0.0);
+
+  TColStd_Array1OfReal aKnots(1, 4);
+  aKnots(1) = 0.0;
+  aKnots(2) = 1.0;
+  aKnots(3) = 2.0;
+  aKnots(4) = 3.0;
+
+  TColStd_Array1OfInteger aMults(1, 4);
+  aMults(1) = 2;
+  aMults(2) = 1;
+  aMults(3) = 1;
+  aMults(4) = 2;
+
+  Handle(Geom_BSplineCurve) aCurve1 = new Geom_BSplineCurve(aPoles, aKnots, aMults, 1);
+  Handle(Geom_BSplineCurve) aCurve2 = Handle(Geom_BSplineCurve)::DownCast(aCurve1->Copy());
+
+  EXPECT_EQ(myHasher(aCurve1), myHasher(aCurve2));
+  EXPECT_TRUE(myHasher(aCurve1, aCurve2));
+}
+
+// ============================================================================
+// Cross-type Conic Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Ellipse_vs_Hyperbola_DifferentComparison)
+{
+  gp_Ax2                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Ellipse)   anEllipse  = new Geom_Ellipse(anAxis, 5.0, 3.0);
+  Handle(Geom_Hyperbola) aHyperbola = new Geom_Hyperbola(anAxis, 5.0, 3.0);
+
+  EXPECT_FALSE(myHasher(anEllipse, aHyperbola));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Circle_vs_Ellipse_DifferentComparison)
+{
+  gp_Ax2               anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle)  aCircle   = new Geom_Circle(anAxis, 5.0);
+  Handle(Geom_Ellipse) anEllipse = new Geom_Ellipse(anAxis, 5.0, 5.0);
+
+  EXPECT_FALSE(myHasher(aCircle, anEllipse));
+}
+
+// ============================================================================
+// Trimmed vs Base Curve Tests
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, TrimmedCurve_vs_BaseCurve_DifferentComparison)
+{
+  Handle(Geom_Line)         aLine    = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_TrimmedCurve) aTrimmed = new Geom_TrimmedCurve(aLine, 0.0, 10.0);
+
+  // Trimmed curve is a different type from base curve
+  EXPECT_FALSE(myHasher(aLine, aTrimmed));
+}
+
+// ============================================================================
+// Edge Cases - Degenerate/Special Values
+// ============================================================================
+
+TEST_F(GeomHash_CurveHasherTest, Circle_VerySmallRadius_CopiedCircles_SameHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 1e-10);
+  Handle(Geom_Circle) aCircle2 = Handle(Geom_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Circle_VeryLargeRadius_CopiedCircles_SameHash)
+{
+  gp_Ax2              anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Circle) aCircle1 = new Geom_Circle(anAxis, 1e10);
+  Handle(Geom_Circle) aCircle2 = Handle(Geom_Circle)::DownCast(aCircle1->Copy());
+
+  EXPECT_EQ(myHasher(aCircle1), myHasher(aCircle2));
+  EXPECT_TRUE(myHasher(aCircle1, aCircle2));
+}
+
+TEST_F(GeomHash_CurveHasherTest, Line_AtOrigin_vs_FarFromOrigin_DifferentHash)
+{
+  Handle(Geom_Line) aLine1 = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_Line) aLine2 = new Geom_Line(gp_Pnt(1e10, 1e10, 1e10), gp_Dir(1.0, 0.0, 0.0));
+
+  EXPECT_NE(myHasher(aLine1), myHasher(aLine2));
+  EXPECT_FALSE(myHasher(aLine1, aLine2));
+}
diff --git a/src/ModelingData/TKG3d/GTests/GeomHash_SurfaceHasher_Test.cxx b/src/ModelingData/TKG3d/GTests/GeomHash_SurfaceHasher_Test.cxx
new file mode 100644 (file)
index 0000000..3f75707
--- /dev/null
@@ -0,0 +1,749 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <gtest/gtest.h>
+
+#include <GeomHash_SurfaceHasher.hxx>
+#include <Geom_Plane.hxx>
+#include <Geom_CylindricalSurface.hxx>
+#include <Geom_ConicalSurface.hxx>
+#include <Geom_SphericalSurface.hxx>
+#include <Geom_ToroidalSurface.hxx>
+#include <Geom_BezierSurface.hxx>
+#include <Geom_BSplineSurface.hxx>
+#include <Geom_SurfaceOfRevolution.hxx>
+#include <Geom_SurfaceOfLinearExtrusion.hxx>
+#include <Geom_RectangularTrimmedSurface.hxx>
+#include <Geom_OffsetSurface.hxx>
+#include <Geom_Line.hxx>
+#include <Geom_Circle.hxx>
+#include <gp_Ax2.hxx>
+#include <gp_Ax3.hxx>
+#include <gp_Pnt.hxx>
+#include <gp_Dir.hxx>
+#include <gp_Vec.hxx>
+#include <TColgp_Array2OfPnt.hxx>
+#include <TColStd_Array1OfReal.hxx>
+#include <TColStd_Array1OfInteger.hxx>
+#include <TColStd_Array2OfReal.hxx>
+
+class GeomHash_SurfaceHasherTest : public ::testing::Test
+{
+protected:
+  GeomHash_SurfaceHasher myHasher;
+};
+
+// ============================================================================
+// Plane Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, Plane_CopiedPlanes_SameHash)
+{
+  gp_Ax3             anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane) aPlane1 = new Geom_Plane(anAxis);
+  Handle(Geom_Plane) aPlane2 = Handle(Geom_Plane)::DownCast(aPlane1->Copy());
+
+  EXPECT_EQ(myHasher(aPlane1), myHasher(aPlane2));
+  EXPECT_TRUE(myHasher(aPlane1, aPlane2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, Plane_DifferentPlanes_DifferentHash)
+{
+  Handle(Geom_Plane) aPlane1 = new Geom_Plane(gp_Ax3(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)));
+  Handle(Geom_Plane) aPlane2 = new Geom_Plane(gp_Ax3(gp_Pnt(0.0, 0.0, 1.0), gp_Dir(0.0, 0.0, 1.0)));
+
+  EXPECT_NE(myHasher(aPlane1), myHasher(aPlane2));
+  EXPECT_FALSE(myHasher(aPlane1, aPlane2));
+}
+
+// ============================================================================
+// Cylindrical Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, CylindricalSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                          anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_CylindricalSurface) aCyl1 = new Geom_CylindricalSurface(anAxis, 5.0);
+  Handle(Geom_CylindricalSurface) aCyl2 = Handle(Geom_CylindricalSurface)::DownCast(aCyl1->Copy());
+
+  EXPECT_EQ(myHasher(aCyl1), myHasher(aCyl2));
+  EXPECT_TRUE(myHasher(aCyl1, aCyl2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, CylindricalSurface_DifferentRadius_DifferentHash)
+{
+  gp_Ax3                          anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_CylindricalSurface) aCyl1 = new Geom_CylindricalSurface(anAxis, 5.0);
+  Handle(Geom_CylindricalSurface) aCyl2 = new Geom_CylindricalSurface(anAxis, 10.0);
+
+  EXPECT_NE(myHasher(aCyl1), myHasher(aCyl2));
+  EXPECT_FALSE(myHasher(aCyl1, aCyl2));
+}
+
+// ============================================================================
+// Conical Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, ConicalSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                      anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_ConicalSurface) aCone1 = new Geom_ConicalSurface(anAxis, M_PI / 6.0, 5.0);
+  Handle(Geom_ConicalSurface) aCone2 = Handle(Geom_ConicalSurface)::DownCast(aCone1->Copy());
+
+  EXPECT_EQ(myHasher(aCone1), myHasher(aCone2));
+  EXPECT_TRUE(myHasher(aCone1, aCone2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, ConicalSurface_DifferentAngle_DifferentHash)
+{
+  gp_Ax3                      anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_ConicalSurface) aCone1 = new Geom_ConicalSurface(anAxis, M_PI / 6.0, 5.0);
+  Handle(Geom_ConicalSurface) aCone2 = new Geom_ConicalSurface(anAxis, M_PI / 4.0, 5.0);
+
+  EXPECT_NE(myHasher(aCone1), myHasher(aCone2));
+  EXPECT_FALSE(myHasher(aCone1, aCone2));
+}
+
+// ============================================================================
+// Spherical Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SphericalSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere1 = new Geom_SphericalSurface(anAxis, 5.0);
+  Handle(Geom_SphericalSurface) aSphere2 =
+    Handle(Geom_SphericalSurface)::DownCast(aSphere1->Copy());
+
+  EXPECT_EQ(myHasher(aSphere1), myHasher(aSphere2));
+  EXPECT_TRUE(myHasher(aSphere1, aSphere2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, SphericalSurface_DifferentRadius_DifferentHash)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere1 = new Geom_SphericalSurface(anAxis, 5.0);
+  Handle(Geom_SphericalSurface) aSphere2 = new Geom_SphericalSurface(anAxis, 10.0);
+
+  EXPECT_NE(myHasher(aSphere1), myHasher(aSphere2));
+  EXPECT_FALSE(myHasher(aSphere1, aSphere2));
+}
+
+// ============================================================================
+// Toroidal Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, ToroidalSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                       anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_ToroidalSurface) aTorus1 = new Geom_ToroidalSurface(anAxis, 10.0, 3.0);
+  Handle(Geom_ToroidalSurface) aTorus2 = Handle(Geom_ToroidalSurface)::DownCast(aTorus1->Copy());
+
+  EXPECT_EQ(myHasher(aTorus1), myHasher(aTorus2));
+  EXPECT_TRUE(myHasher(aTorus1, aTorus2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, ToroidalSurface_DifferentRadii_DifferentHash)
+{
+  gp_Ax3                       anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_ToroidalSurface) aTorus1 = new Geom_ToroidalSurface(anAxis, 10.0, 3.0);
+  Handle(Geom_ToroidalSurface) aTorus2 = new Geom_ToroidalSurface(anAxis, 10.0, 5.0);
+
+  EXPECT_NE(myHasher(aTorus1), myHasher(aTorus2));
+  EXPECT_FALSE(myHasher(aTorus1, aTorus2));
+}
+
+// ============================================================================
+// BezierSurface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BezierSurface_CopiedSurfaces_SameHash)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  Handle(Geom_BezierSurface) aSurf1 = new Geom_BezierSurface(aPoles);
+  Handle(Geom_BezierSurface) aSurf2 = Handle(Geom_BezierSurface)::DownCast(aSurf1->Copy());
+
+  EXPECT_EQ(myHasher(aSurf1), myHasher(aSurf2));
+  EXPECT_TRUE(myHasher(aSurf1, aSurf2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, BezierSurface_DifferentPoles_DifferentComparison)
+{
+  TColgp_Array2OfPnt aPoles1(1, 2, 1, 2);
+  aPoles1(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles1(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles1(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles1(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColgp_Array2OfPnt aPoles2(1, 2, 1, 2);
+  aPoles2(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles2(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles2(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles2(2, 2) = gp_Pnt(1.0, 1.0, 2.0); // Different
+
+  Handle(Geom_BezierSurface) aSurf1 = new Geom_BezierSurface(aPoles1);
+  Handle(Geom_BezierSurface) aSurf2 = new Geom_BezierSurface(aPoles2);
+
+  // Hash may be same (metadata only), but comparison should differ
+  EXPECT_FALSE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// BSplineSurface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BSplineSurface_CopiedSurfaces_SameHash)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColStd_Array1OfReal aUKnots(1, 2), aVKnots(1, 2);
+  aUKnots(1) = 0.0;
+  aUKnots(2) = 1.0;
+  aVKnots(1) = 0.0;
+  aVKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aUMults(1, 2), aVMults(1, 2);
+  aUMults(1) = 2;
+  aUMults(2) = 2;
+  aVMults(1) = 2;
+  aVMults(2) = 2;
+
+  Handle(Geom_BSplineSurface) aSurf1 =
+    new Geom_BSplineSurface(aPoles, aUKnots, aVKnots, aUMults, aVMults, 1, 1);
+  Handle(Geom_BSplineSurface) aSurf2 = Handle(Geom_BSplineSurface)::DownCast(aSurf1->Copy());
+
+  EXPECT_EQ(myHasher(aSurf1), myHasher(aSurf2));
+  EXPECT_TRUE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// SurfaceOfRevolution Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SurfaceOfRevolution_CopiedSurfaces_SameHash)
+{
+  Handle(Geom_Line) aLine = new Geom_Line(gp_Pnt(1.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax1            anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SurfaceOfRevolution) aRev1 = new Geom_SurfaceOfRevolution(aLine, anAxis);
+  Handle(Geom_SurfaceOfRevolution) aRev2 =
+    Handle(Geom_SurfaceOfRevolution)::DownCast(aRev1->Copy());
+
+  EXPECT_EQ(myHasher(aRev1), myHasher(aRev2));
+  EXPECT_TRUE(myHasher(aRev1, aRev2));
+}
+
+// ============================================================================
+// SurfaceOfLinearExtrusion Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SurfaceOfLinearExtrusion_CopiedSurfaces_SameHash)
+{
+  Handle(Geom_Line) aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  gp_Dir            aDir(0.0, 0.0, 1.0);
+  Handle(Geom_SurfaceOfLinearExtrusion) anExt1 = new Geom_SurfaceOfLinearExtrusion(aLine, aDir);
+  Handle(Geom_SurfaceOfLinearExtrusion) anExt2 =
+    Handle(Geom_SurfaceOfLinearExtrusion)::DownCast(anExt1->Copy());
+
+  EXPECT_EQ(myHasher(anExt1), myHasher(anExt2));
+  EXPECT_TRUE(myHasher(anExt1, anExt2));
+}
+
+// ============================================================================
+// RectangularTrimmedSurface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, RectangularTrimmedSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)                     aPlane = new Geom_Plane(anAxis);
+  Handle(Geom_RectangularTrimmedSurface) aTrimmed1 =
+    new Geom_RectangularTrimmedSurface(aPlane, 0.0, 10.0, 0.0, 10.0);
+  Handle(Geom_RectangularTrimmedSurface) aTrimmed2 =
+    Handle(Geom_RectangularTrimmedSurface)::DownCast(aTrimmed1->Copy());
+
+  EXPECT_EQ(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_TRUE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, RectangularTrimmedSurface_DifferentBounds_DifferentHash)
+{
+  gp_Ax3                                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)                     aPlane = new Geom_Plane(anAxis);
+  Handle(Geom_RectangularTrimmedSurface) aTrimmed1 =
+    new Geom_RectangularTrimmedSurface(aPlane, 0.0, 10.0, 0.0, 10.0);
+  Handle(Geom_RectangularTrimmedSurface) aTrimmed2 =
+    new Geom_RectangularTrimmedSurface(aPlane, 0.0, 20.0, 0.0, 10.0);
+
+  EXPECT_NE(myHasher(aTrimmed1), myHasher(aTrimmed2));
+  EXPECT_FALSE(myHasher(aTrimmed1, aTrimmed2));
+}
+
+// ============================================================================
+// OffsetSurface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, OffsetSurface_CopiedSurfaces_SameHash)
+{
+  gp_Ax3                     anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)         aPlane    = new Geom_Plane(anAxis);
+  Handle(Geom_OffsetSurface) anOffset1 = new Geom_OffsetSurface(aPlane, 5.0);
+  Handle(Geom_OffsetSurface) anOffset2 = Handle(Geom_OffsetSurface)::DownCast(anOffset1->Copy());
+
+  EXPECT_EQ(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_TRUE(myHasher(anOffset1, anOffset2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, OffsetSurface_DifferentOffset_DifferentHash)
+{
+  gp_Ax3                     anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)         aPlane    = new Geom_Plane(anAxis);
+  Handle(Geom_OffsetSurface) anOffset1 = new Geom_OffsetSurface(aPlane, 5.0);
+  Handle(Geom_OffsetSurface) anOffset2 = new Geom_OffsetSurface(aPlane, 10.0);
+
+  EXPECT_NE(myHasher(anOffset1), myHasher(anOffset2));
+  EXPECT_FALSE(myHasher(anOffset1, anOffset2));
+}
+
+// ============================================================================
+// Cross-type Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, DifferentTypes_DifferentComparison)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)            aPlane  = new Geom_Plane(anAxis);
+  Handle(Geom_SphericalSurface) aSphere = new Geom_SphericalSurface(anAxis, 5.0);
+
+  EXPECT_FALSE(myHasher(aPlane, aSphere));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, NullSurfaces_HandledCorrectly)
+{
+  Handle(Geom_Surface) aNullSurface;
+  gp_Ax3               anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)   aPlane = new Geom_Plane(anAxis);
+
+  EXPECT_EQ(myHasher(aNullSurface), 0u);
+  EXPECT_TRUE(myHasher(aNullSurface, aNullSurface));
+  EXPECT_FALSE(myHasher(aPlane, aNullSurface));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, SameObject_Equal)
+{
+  gp_Ax3             anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane) aPlane = new Geom_Plane(anAxis);
+  EXPECT_TRUE(myHasher(aPlane, aPlane));
+}
+
+// ============================================================================
+// Weighted BSpline Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BSplineSurface_Weighted_CopiedSurfaces_SameHash)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColStd_Array2OfReal aWeights(1, 2, 1, 2);
+  aWeights(1, 1) = 1.0;
+  aWeights(1, 2) = 1.0;
+  aWeights(2, 1) = 1.0;
+  aWeights(2, 2) = 2.0;
+
+  TColStd_Array1OfReal aUKnots(1, 2), aVKnots(1, 2);
+  aUKnots(1) = 0.0;
+  aUKnots(2) = 1.0;
+  aVKnots(1) = 0.0;
+  aVKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aUMults(1, 2), aVMults(1, 2);
+  aUMults(1) = 2;
+  aUMults(2) = 2;
+  aVMults(1) = 2;
+  aVMults(2) = 2;
+
+  Handle(Geom_BSplineSurface) aSurf1 =
+    new Geom_BSplineSurface(aPoles, aWeights, aUKnots, aVKnots, aUMults, aVMults, 1, 1);
+  Handle(Geom_BSplineSurface) aSurf2 = Handle(Geom_BSplineSurface)::DownCast(aSurf1->Copy());
+
+  EXPECT_EQ(myHasher(aSurf1), myHasher(aSurf2));
+  EXPECT_TRUE(myHasher(aSurf1, aSurf2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, BSplineSurface_DifferentWeights_DifferentComparison)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColStd_Array2OfReal aWeights1(1, 2, 1, 2);
+  aWeights1(1, 1) = 1.0;
+  aWeights1(1, 2) = 1.0;
+  aWeights1(2, 1) = 1.0;
+  aWeights1(2, 2) = 2.0;
+
+  TColStd_Array2OfReal aWeights2(1, 2, 1, 2);
+  aWeights2(1, 1) = 1.0;
+  aWeights2(1, 2) = 1.0;
+  aWeights2(2, 1) = 1.0;
+  aWeights2(2, 2) = 3.0; // Different
+
+  TColStd_Array1OfReal aUKnots(1, 2), aVKnots(1, 2);
+  aUKnots(1) = 0.0;
+  aUKnots(2) = 1.0;
+  aVKnots(1) = 0.0;
+  aVKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aUMults(1, 2), aVMults(1, 2);
+  aUMults(1) = 2;
+  aUMults(2) = 2;
+  aVMults(1) = 2;
+  aVMults(2) = 2;
+
+  Handle(Geom_BSplineSurface) aSurf1 =
+    new Geom_BSplineSurface(aPoles, aWeights1, aUKnots, aVKnots, aUMults, aVMults, 1, 1);
+  Handle(Geom_BSplineSurface) aSurf2 =
+    new Geom_BSplineSurface(aPoles, aWeights2, aUKnots, aVKnots, aUMults, aVMults, 1, 1);
+
+  EXPECT_FALSE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// Weighted Bezier Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BezierSurface_Weighted_CopiedSurfaces_SameHash)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColStd_Array2OfReal aWeights(1, 2, 1, 2);
+  aWeights(1, 1) = 1.0;
+  aWeights(1, 2) = 1.0;
+  aWeights(2, 1) = 1.0;
+  aWeights(2, 2) = 2.0;
+
+  Handle(Geom_BezierSurface) aSurf1 = new Geom_BezierSurface(aPoles, aWeights);
+  Handle(Geom_BezierSurface) aSurf2 = Handle(Geom_BezierSurface)::DownCast(aSurf1->Copy());
+
+  EXPECT_EQ(myHasher(aSurf1), myHasher(aSurf2));
+  EXPECT_TRUE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// Axis Orientation Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, Plane_DifferentNormal_DifferentHash)
+{
+  gp_Ax3             anAxis1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax3             anAxis2(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0));
+  Handle(Geom_Plane) aPlane1 = new Geom_Plane(anAxis1);
+  Handle(Geom_Plane) aPlane2 = new Geom_Plane(anAxis2);
+
+  EXPECT_NE(myHasher(aPlane1), myHasher(aPlane2));
+  EXPECT_FALSE(myHasher(aPlane1, aPlane2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, CylindricalSurface_DifferentAxis_DifferentHash)
+{
+  gp_Ax3                          anAxis1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax3                          anAxis2(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  Handle(Geom_CylindricalSurface) aCyl1 = new Geom_CylindricalSurface(anAxis1, 5.0);
+  Handle(Geom_CylindricalSurface) aCyl2 = new Geom_CylindricalSurface(anAxis2, 5.0);
+
+  EXPECT_NE(myHasher(aCyl1), myHasher(aCyl2));
+  EXPECT_FALSE(myHasher(aCyl1, aCyl2));
+}
+
+// ============================================================================
+// Revolution with Different Basis Curve Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SurfaceOfRevolution_DifferentBasisCurve_DifferentComparison)
+{
+  Handle(Geom_Line) aLine1 = new Geom_Line(gp_Pnt(1.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Line) aLine2 = new Geom_Line(gp_Pnt(2.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax1            anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SurfaceOfRevolution) aRev1 = new Geom_SurfaceOfRevolution(aLine1, anAxis);
+  Handle(Geom_SurfaceOfRevolution) aRev2 = new Geom_SurfaceOfRevolution(aLine2, anAxis);
+
+  EXPECT_FALSE(myHasher(aRev1, aRev2));
+}
+
+// ============================================================================
+// Extrusion with Different Direction Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SurfaceOfLinearExtrusion_DifferentDirection_DifferentHash)
+{
+  Handle(Geom_Line) aLine = new Geom_Line(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0));
+  gp_Dir            aDir1(0.0, 0.0, 1.0);
+  gp_Dir            aDir2(0.0, 1.0, 0.0);
+  Handle(Geom_SurfaceOfLinearExtrusion) anExt1 = new Geom_SurfaceOfLinearExtrusion(aLine, aDir1);
+  Handle(Geom_SurfaceOfLinearExtrusion) anExt2 = new Geom_SurfaceOfLinearExtrusion(aLine, aDir2);
+
+  EXPECT_NE(myHasher(anExt1), myHasher(anExt2));
+  EXPECT_FALSE(myHasher(anExt1, anExt2));
+}
+
+// ============================================================================
+// BSpline Surface with Different Knots Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BSplineSurface_DifferentKnots_DifferentComparison)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  TColStd_Array1OfReal aUKnots1(1, 2), aUKnots2(1, 2), aVKnots(1, 2);
+  aUKnots1(1) = 0.0;
+  aUKnots1(2) = 1.0;
+  aUKnots2(1) = 0.0;
+  aUKnots2(2) = 2.0; // Different
+  aVKnots(1)  = 0.0;
+  aVKnots(2)  = 1.0;
+
+  TColStd_Array1OfInteger aUMults(1, 2), aVMults(1, 2);
+  aUMults(1) = 2;
+  aUMults(2) = 2;
+  aVMults(1) = 2;
+  aVMults(2) = 2;
+
+  Handle(Geom_BSplineSurface) aSurf1 =
+    new Geom_BSplineSurface(aPoles, aUKnots1, aVKnots, aUMults, aVMults, 1, 1);
+  Handle(Geom_BSplineSurface) aSurf2 =
+    new Geom_BSplineSurface(aPoles, aUKnots2, aVKnots, aUMults, aVMults, 1, 1);
+
+  EXPECT_FALSE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// Transformed Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, Plane_Translated_DifferentHash)
+{
+  gp_Ax3             anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane) aPlane1 = new Geom_Plane(anAxis);
+  Handle(Geom_Plane) aPlane2 = Handle(Geom_Plane)::DownCast(aPlane1->Copy());
+  aPlane2->Translate(gp_Vec(0.0, 0.0, 1.0));
+
+  EXPECT_NE(myHasher(aPlane1), myHasher(aPlane2));
+  EXPECT_FALSE(myHasher(aPlane1, aPlane2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, Sphere_Scaled_DifferentHash)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere1 = new Geom_SphericalSurface(anAxis, 5.0);
+  Handle(Geom_SphericalSurface) aSphere2 =
+    Handle(Geom_SphericalSurface)::DownCast(aSphere1->Copy());
+  aSphere2->Scale(gp_Pnt(0.0, 0.0, 0.0), 2.0);
+
+  EXPECT_NE(myHasher(aSphere1), myHasher(aSphere2));
+  EXPECT_FALSE(myHasher(aSphere1, aSphere2));
+}
+
+// ============================================================================
+// UReversed/VReversed Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BezierSurface_UReversed_DifferentComparison)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  Handle(Geom_BezierSurface) aSurf1 = new Geom_BezierSurface(aPoles);
+  Handle(Geom_BezierSurface) aSurf2 = Handle(Geom_BezierSurface)::DownCast(aSurf1->UReversed());
+
+  EXPECT_FALSE(myHasher(aSurf1, aSurf2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, BezierSurface_VReversed_DifferentComparison)
+{
+  TColgp_Array2OfPnt aPoles(1, 2, 1, 2);
+  aPoles(1, 1) = gp_Pnt(0.0, 0.0, 0.0);
+  aPoles(1, 2) = gp_Pnt(1.0, 0.0, 0.0);
+  aPoles(2, 1) = gp_Pnt(0.0, 1.0, 0.0);
+  aPoles(2, 2) = gp_Pnt(1.0, 1.0, 1.0);
+
+  Handle(Geom_BezierSurface) aSurf1 = new Geom_BezierSurface(aPoles);
+  Handle(Geom_BezierSurface) aSurf2 = Handle(Geom_BezierSurface)::DownCast(aSurf1->VReversed());
+
+  EXPECT_FALSE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// Higher Degree BSpline Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, BSplineSurface_HigherDegree_CopiedSurfaces_SameHash)
+{
+  TColgp_Array2OfPnt aPoles(1, 4, 1, 4);
+  for (Standard_Integer i = 1; i <= 4; ++i)
+  {
+    for (Standard_Integer j = 1; j <= 4; ++j)
+    {
+      aPoles(i, j) = gp_Pnt((i - 1) * 1.0, (j - 1) * 1.0, (i + j) * 0.1);
+    }
+  }
+
+  TColStd_Array1OfReal aUKnots(1, 2), aVKnots(1, 2);
+  aUKnots(1) = 0.0;
+  aUKnots(2) = 1.0;
+  aVKnots(1) = 0.0;
+  aVKnots(2) = 1.0;
+
+  TColStd_Array1OfInteger aUMults(1, 2), aVMults(1, 2);
+  aUMults(1) = 4;
+  aUMults(2) = 4;
+  aVMults(1) = 4;
+  aVMults(2) = 4;
+
+  Handle(Geom_BSplineSurface) aSurf1 =
+    new Geom_BSplineSurface(aPoles, aUKnots, aVKnots, aUMults, aVMults, 3, 3);
+  Handle(Geom_BSplineSurface) aSurf2 = Handle(Geom_BSplineSurface)::DownCast(aSurf1->Copy());
+
+  EXPECT_EQ(myHasher(aSurf1), myHasher(aSurf2));
+  EXPECT_TRUE(myHasher(aSurf1, aSurf2));
+}
+
+// ============================================================================
+// Cross-type Elementary Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, Cylinder_vs_Cone_DifferentComparison)
+{
+  gp_Ax3                          anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_CylindricalSurface) aCylinder = new Geom_CylindricalSurface(anAxis, 5.0);
+  Handle(Geom_ConicalSurface)     aCone     = new Geom_ConicalSurface(anAxis, M_PI / 6.0, 5.0);
+
+  EXPECT_FALSE(myHasher(aCylinder, aCone));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, Sphere_vs_Torus_DifferentComparison)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere = new Geom_SphericalSurface(anAxis, 5.0);
+  Handle(Geom_ToroidalSurface)  aTorus  = new Geom_ToroidalSurface(anAxis, 5.0, 1.0);
+
+  EXPECT_FALSE(myHasher(aSphere, aTorus));
+}
+
+// ============================================================================
+// Trimmed vs Base Surface Tests
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, RectangularTrimmedSurface_vs_BaseSurface_DifferentComparison)
+{
+  gp_Ax3                                 anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)                     aPlane = new Geom_Plane(anAxis);
+  Handle(Geom_RectangularTrimmedSurface) aTrimmed =
+    new Geom_RectangularTrimmedSurface(aPlane, 0.0, 10.0, 0.0, 10.0);
+
+  EXPECT_FALSE(myHasher(aPlane, aTrimmed));
+}
+
+// ============================================================================
+// Edge Cases - Degenerate/Special Values
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, Sphere_VerySmallRadius_CopiedSpheres_SameHash)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere1 = new Geom_SphericalSurface(anAxis, 1e-10);
+  Handle(Geom_SphericalSurface) aSphere2 =
+    Handle(Geom_SphericalSurface)::DownCast(aSphere1->Copy());
+
+  EXPECT_EQ(myHasher(aSphere1), myHasher(aSphere2));
+  EXPECT_TRUE(myHasher(aSphere1, aSphere2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, Sphere_VeryLargeRadius_CopiedSpheres_SameHash)
+{
+  gp_Ax3                        anAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SphericalSurface) aSphere1 = new Geom_SphericalSurface(anAxis, 1e10);
+  Handle(Geom_SphericalSurface) aSphere2 =
+    Handle(Geom_SphericalSurface)::DownCast(aSphere1->Copy());
+
+  EXPECT_EQ(myHasher(aSphere1), myHasher(aSphere2));
+  EXPECT_TRUE(myHasher(aSphere1, aSphere2));
+}
+
+TEST_F(GeomHash_SurfaceHasherTest, Plane_AtOrigin_vs_FarFromOrigin_DifferentHash)
+{
+  gp_Ax3             anAxis1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax3             anAxis2(gp_Pnt(1e10, 1e10, 1e10), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane) aPlane1 = new Geom_Plane(anAxis1);
+  Handle(Geom_Plane) aPlane2 = new Geom_Plane(anAxis2);
+
+  EXPECT_NE(myHasher(aPlane1), myHasher(aPlane2));
+  EXPECT_FALSE(myHasher(aPlane1, aPlane2));
+}
+
+// ============================================================================
+// Revolution with Circle Basis (forms Torus-like)
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, SurfaceOfRevolution_CircleBasis_CopiedSurfaces_SameHash)
+{
+  gp_Ax2                           aCircleAxis(gp_Pnt(5.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0));
+  Handle(Geom_Circle)              aCircle = new Geom_Circle(aCircleAxis, 1.0);
+  gp_Ax1                           aRevAxis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_SurfaceOfRevolution) aRev1 = new Geom_SurfaceOfRevolution(aCircle, aRevAxis);
+  Handle(Geom_SurfaceOfRevolution) aRev2 =
+    Handle(Geom_SurfaceOfRevolution)::DownCast(aRev1->Copy());
+
+  EXPECT_EQ(myHasher(aRev1), myHasher(aRev2));
+  EXPECT_TRUE(myHasher(aRev1, aRev2));
+}
+
+// ============================================================================
+// Offset Surface with Different Base
+// ============================================================================
+
+TEST_F(GeomHash_SurfaceHasherTest, OffsetSurface_DifferentBaseSurface_DifferentComparison)
+{
+  gp_Ax3                     anAxis1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  gp_Ax3                     anAxis2(gp_Pnt(1.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0));
+  Handle(Geom_Plane)         aPlane1   = new Geom_Plane(anAxis1);
+  Handle(Geom_Plane)         aPlane2   = new Geom_Plane(anAxis2);
+  Handle(Geom_OffsetSurface) anOffset1 = new Geom_OffsetSurface(aPlane1, 5.0);
+  Handle(Geom_OffsetSurface) anOffset2 = new Geom_OffsetSurface(aPlane2, 5.0);
+
+  EXPECT_FALSE(myHasher(anOffset1, anOffset2));
+}
diff --git a/src/ModelingData/TKG3d/GeomHash/FILES.cmake b/src/ModelingData/TKG3d/GeomHash/FILES.cmake
new file mode 100644 (file)
index 0000000..6574490
--- /dev/null
@@ -0,0 +1,38 @@
+# Source files for GeomHash package
+set(OCCT_GeomHash_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}")
+
+set(OCCT_GeomHash_FILES
+  # Foundational Hashers
+  GeomHash_PointHasher.pxx
+  GeomHash_DirectionHasher.pxx
+  GeomHash_VectorHasher.pxx
+  GeomHash_AxisPlacement.pxx
+
+  # Surface Hashers
+  GeomHash_PlaneHasher.pxx
+  GeomHash_CylindricalSurfaceHasher.pxx
+  GeomHash_ConicalSurfaceHasher.pxx
+  GeomHash_SphericalSurfaceHasher.pxx
+  GeomHash_ToroidalSurfaceHasher.pxx
+  GeomHash_SurfaceOfRevolutionHasher.pxx
+  GeomHash_SurfaceOfLinearExtrusionHasher.pxx
+  GeomHash_BezierSurfaceHasher.pxx
+  GeomHash_BSplineSurfaceHasher.pxx
+  GeomHash_RectangularTrimmedSurfaceHasher.pxx
+  GeomHash_OffsetSurfaceHasher.pxx
+  GeomHash_SurfaceHasher.hxx
+  GeomHash_SurfaceHasher.cxx
+
+  # Curve Hashers
+  GeomHash_LineHasher.pxx
+  GeomHash_CircleHasher.pxx
+  GeomHash_EllipseHasher.pxx
+  GeomHash_HyperbolaHasher.pxx
+  GeomHash_ParabolaHasher.pxx
+  GeomHash_BezierCurveHasher.pxx
+  GeomHash_BSplineCurveHasher.pxx
+  GeomHash_TrimmedCurveHasher.pxx
+  GeomHash_OffsetCurveHasher.pxx
+  GeomHash_CurveHasher.hxx
+  GeomHash_CurveHasher.cxx
+)
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_AxisPlacement.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_AxisPlacement.pxx
new file mode 100644 (file)
index 0000000..c7174b0
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_AxisPlacement_HeaderFile
+#define _GeomHash_AxisPlacement_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Ax2.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <GeomHash_DirectionHasher.pxx>
+
+//! OCCT-style hasher for gp_Ax2 (axis placement).
+//! Used for geometry deduplication.
+//! Compositional hasher using PointHasher and DirectionHasher.
+struct GeomHash_AxisPlacement
+{
+  // Hashes the axis placement by location, axis direction, and X direction.
+  std::size_t operator()(const gp_Ax2& theAxisPlacement) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirectionHasher;
+
+    const std::size_t aHashes[3] = {aPointHasher(theAxisPlacement.Location()),
+                                    aDirectionHasher(theAxisPlacement.Direction()),
+                                    aDirectionHasher(theAxisPlacement.XDirection())};
+
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two axis placements.
+  bool operator()(const gp_Ax2& theAxisPlacement1, const gp_Ax2& theAxisPlacement2) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirectionHasher;
+
+    return aPointHasher(theAxisPlacement1.Location(), theAxisPlacement2.Location())
+           && aDirectionHasher(theAxisPlacement1.Direction(), theAxisPlacement2.Direction())
+           && aDirectionHasher(theAxisPlacement1.XDirection(), theAxisPlacement2.XDirection());
+  }
+};
+
+#endif // _GeomHash_AxisPlacement_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineCurveHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineCurveHasher.pxx
new file mode 100644 (file)
index 0000000..01f6948
--- /dev/null
@@ -0,0 +1,98 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_BSplineCurveHasher_HeaderFile
+#define _GeomHash_BSplineCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_BSplineCurve.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_BSplineCurve (3D B-spline curve).
+//! Used for geometry deduplication.
+//! Hashes only metadata (degree, pole count, knot count, rationality) for efficiency.
+struct GeomHash_BSplineCurveHasher
+{
+  // Hashes the B-spline curve metadata only.
+  std::size_t operator()(const Handle(Geom_BSplineCurve)& theCurve) const noexcept
+  {
+    const std::size_t aHashes[4] = {opencascade::hash(theCurve->Degree()),
+                                    opencascade::hash(theCurve->NbPoles()),
+                                    opencascade::hash(theCurve->NbKnots()),
+                                    opencascade::hash(static_cast<int>(theCurve->IsRational()))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two B-spline curves by full geometric data.
+  bool operator()(const Handle(Geom_BSplineCurve)& theCurve1,
+                  const Handle(Geom_BSplineCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theCurve1->Degree() != theCurve2->Degree())
+    {
+      return false;
+    }
+
+    // Compare knot counts
+    if (theCurve1->NbKnots() != theCurve2->NbKnots())
+    {
+      return false;
+    }
+
+    // Compare knots and multiplicities
+    for (int i = 1; i <= theCurve1->NbKnots(); ++i)
+    {
+      if (std::abs(theCurve1->Knot(i) - theCurve2->Knot(i)) > aTolerance
+          || theCurve1->Multiplicity(i) != theCurve2->Multiplicity(i))
+      {
+        return false;
+      }
+    }
+
+    // Compare rationality
+    if (theCurve1->IsRational() != theCurve2->IsRational())
+    {
+      return false;
+    }
+
+    const GeomHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+    {
+      if (!aPointHasher(theCurve1->Pole(i), theCurve2->Pole(i)))
+      {
+        return false;
+      }
+    }
+
+    // Compare weights if rational
+    if (theCurve1->IsRational())
+    {
+      for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+      {
+        if (std::abs(theCurve1->Weight(i) - theCurve2->Weight(i)) > aTolerance)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _GeomHash_BSplineCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_BSplineSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..8b8d94b
--- /dev/null
@@ -0,0 +1,122 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_BSplineSurfaceHasher_HeaderFile
+#define _GeomHash_BSplineSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_BSplineSurface.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_BSplineSurface.
+//! Used for geometry deduplication.
+//! Hashes only metadata (degrees, pole counts, knot counts, rationality) for efficiency.
+struct GeomHash_BSplineSurfaceHasher
+{
+  // Hashes the B-spline surface metadata only.
+  std::size_t operator()(const Handle(Geom_BSplineSurface)& theSurface) const noexcept
+  {
+    const std::size_t aHashes[7] = {
+      opencascade::hash(theSurface->UDegree()),
+      opencascade::hash(theSurface->VDegree()),
+      opencascade::hash(theSurface->NbUPoles()),
+      opencascade::hash(theSurface->NbVPoles()),
+      opencascade::hash(theSurface->NbUKnots()),
+      opencascade::hash(theSurface->NbVKnots()),
+      opencascade::hash(static_cast<int>(theSurface->IsURational())
+                        | (static_cast<int>(theSurface->IsVRational()) << 1))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two B-spline surfaces by full geometric data.
+  bool operator()(const Handle(Geom_BSplineSurface)& theSurface1,
+                  const Handle(Geom_BSplineSurface)& theSurface2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theSurface1->UDegree() != theSurface2->UDegree()
+        || theSurface1->VDegree() != theSurface2->VDegree())
+    {
+      return false;
+    }
+
+    // Compare knot counts
+    if (theSurface1->NbUKnots() != theSurface2->NbUKnots()
+        || theSurface1->NbVKnots() != theSurface2->NbVKnots())
+    {
+      return false;
+    }
+
+    // Compare U knots and multiplicities
+    for (int i = 1; i <= theSurface1->NbUKnots(); ++i)
+    {
+      if (std::abs(theSurface1->UKnot(i) - theSurface2->UKnot(i)) > aTolerance
+          || theSurface1->UMultiplicity(i) != theSurface2->UMultiplicity(i))
+      {
+        return false;
+      }
+    }
+
+    // Compare V knots and multiplicities
+    for (int i = 1; i <= theSurface1->NbVKnots(); ++i)
+    {
+      if (std::abs(theSurface1->VKnot(i) - theSurface2->VKnot(i)) > aTolerance
+          || theSurface1->VMultiplicity(i) != theSurface2->VMultiplicity(i))
+      {
+        return false;
+      }
+    }
+
+    // Compare rationality
+    if (theSurface1->IsURational() != theSurface2->IsURational()
+        || theSurface1->IsVRational() != theSurface2->IsVRational())
+    {
+      return false;
+    }
+
+    const GeomHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theSurface1->NbUPoles(); ++i)
+    {
+      for (int j = 1; j <= theSurface1->NbVPoles(); ++j)
+      {
+        if (!aPointHasher(theSurface1->Pole(i, j), theSurface2->Pole(i, j)))
+        {
+          return false;
+        }
+      }
+    }
+
+    // Compare weights if rational
+    if (theSurface1->IsURational() || theSurface1->IsVRational())
+    {
+      for (int i = 1; i <= theSurface1->NbUPoles(); ++i)
+      {
+        for (int j = 1; j <= theSurface1->NbVPoles(); ++j)
+        {
+          if (std::abs(theSurface1->Weight(i, j) - theSurface2->Weight(i, j)) > aTolerance)
+          {
+            return false;
+          }
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _GeomHash_BSplineSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_BezierCurveHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_BezierCurveHasher.pxx
new file mode 100644 (file)
index 0000000..d1f87f4
--- /dev/null
@@ -0,0 +1,81 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_BezierCurveHasher_HeaderFile
+#define _GeomHash_BezierCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_BezierCurve.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_BezierCurve (3D Bezier curve).
+//! Used for geometry deduplication.
+//! Hashes only metadata (degree, pole count, rationality) for efficiency.
+struct GeomHash_BezierCurveHasher
+{
+  // Hashes the Bezier curve metadata only.
+  std::size_t operator()(const Handle(Geom_BezierCurve)& theCurve) const noexcept
+  {
+    const std::size_t aHashes[3] = {opencascade::hash(theCurve->Degree()),
+                                    opencascade::hash(theCurve->NbPoles()),
+                                    opencascade::hash(static_cast<int>(theCurve->IsRational()))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two Bezier curves by full geometric data.
+  bool operator()(const Handle(Geom_BezierCurve)& theCurve1,
+                  const Handle(Geom_BezierCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theCurve1->Degree() != theCurve2->Degree())
+    {
+      return false;
+    }
+
+    // Compare rationality
+    if (theCurve1->IsRational() != theCurve2->IsRational())
+    {
+      return false;
+    }
+
+    const GeomHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+    {
+      if (!aPointHasher(theCurve1->Pole(i), theCurve2->Pole(i)))
+      {
+        return false;
+      }
+    }
+
+    // Compare weights if rational
+    if (theCurve1->IsRational())
+    {
+      for (int i = 1; i <= theCurve1->NbPoles(); ++i)
+      {
+        if (std::abs(theCurve1->Weight(i) - theCurve2->Weight(i)) > aTolerance)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _GeomHash_BezierCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_BezierSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_BezierSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..52cf68e
--- /dev/null
@@ -0,0 +1,93 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_BezierSurfaceHasher_HeaderFile
+#define _GeomHash_BezierSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_BezierSurface.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_BezierSurface.
+//! Used for geometry deduplication.
+//! Hashes only metadata (degrees, pole counts, rationality) for efficiency.
+struct GeomHash_BezierSurfaceHasher
+{
+  // Hashes the Bezier surface metadata only.
+  std::size_t operator()(const Handle(Geom_BezierSurface)& theSurface) const noexcept
+  {
+    const std::size_t aHashes[5] = {
+      opencascade::hash(theSurface->UDegree()),
+      opencascade::hash(theSurface->VDegree()),
+      opencascade::hash(theSurface->NbUPoles()),
+      opencascade::hash(theSurface->NbVPoles()),
+      opencascade::hash(static_cast<int>(theSurface->IsURational())
+                        | (static_cast<int>(theSurface->IsVRational()) << 1))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two Bezier surfaces by full geometric data.
+  bool operator()(const Handle(Geom_BezierSurface)& theSurface1,
+                  const Handle(Geom_BezierSurface)& theSurface2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    // Compare degrees
+    if (theSurface1->UDegree() != theSurface2->UDegree()
+        || theSurface1->VDegree() != theSurface2->VDegree())
+    {
+      return false;
+    }
+
+    // Compare rationality
+    if (theSurface1->IsURational() != theSurface2->IsURational()
+        || theSurface1->IsVRational() != theSurface2->IsVRational())
+    {
+      return false;
+    }
+
+    const GeomHash_PointHasher aPointHasher;
+
+    // Compare poles
+    for (int i = 1; i <= theSurface1->NbUPoles(); ++i)
+    {
+      for (int j = 1; j <= theSurface1->NbVPoles(); ++j)
+      {
+        if (!aPointHasher(theSurface1->Pole(i, j), theSurface2->Pole(i, j)))
+        {
+          return false;
+        }
+      }
+    }
+
+    // Compare weights if rational
+    if (theSurface1->IsURational() || theSurface1->IsVRational())
+    {
+      for (int i = 1; i <= theSurface1->NbUPoles(); ++i)
+      {
+        for (int j = 1; j <= theSurface1->NbVPoles(); ++j)
+        {
+          if (std::abs(theSurface1->Weight(i, j) - theSurface2->Weight(i, j)) > aTolerance)
+          {
+            return false;
+          }
+        }
+      }
+    }
+
+    return true;
+  }
+};
+
+#endif // _GeomHash_BezierSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_CircleHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_CircleHasher.pxx
new file mode 100644 (file)
index 0000000..59f488e
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_CircleHasher_HeaderFile
+#define _GeomHash_CircleHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Circle.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_Circle (3D circle).
+//! Used for geometry deduplication.
+struct GeomHash_CircleHasher
+{
+  // Hashes the circle by its position and radius.
+  std::size_t operator()(const Handle(Geom_Circle)& theCircle) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[2] = {
+      anAxisHasher(theCircle->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCircle->Radius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two circles by their positions and radii.
+  bool operator()(const Handle(Geom_Circle)& theCircle1,
+                  const Handle(Geom_Circle)& theCircle2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theCircle1->Position(), theCircle2->Position())
+           && std::abs(theCircle1->Radius() - theCircle2->Radius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_CircleHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_ConicalSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_ConicalSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..ceefad3
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_ConicalSurfaceHasher_HeaderFile
+#define _GeomHash_ConicalSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_ConicalSurface.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_ConicalSurface.
+//! Used for geometry deduplication.
+struct GeomHash_ConicalSurfaceHasher
+{
+  // Hashes the cone by its position, apex radius, and semi-angle.
+  std::size_t operator()(const Handle(Geom_ConicalSurface)& theCone) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[3] = {
+      anAxisHasher(theCone->Position().Ax2()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCone->RefRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theCone->SemiAngle() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two cones by their positions, radii, and semi-angles.
+  bool operator()(const Handle(Geom_ConicalSurface)& theCone1,
+                  const Handle(Geom_ConicalSurface)& theCone2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(theCone1->Position().Ax2(), theCone2->Position().Ax2())
+           && std::abs(theCone1->RefRadius() - theCone2->RefRadius()) <= aTolerance
+           && std::abs(theCone1->SemiAngle() - theCone2->SemiAngle()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_ConicalSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.cxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.cxx
new file mode 100644 (file)
index 0000000..f742e6c
--- /dev/null
@@ -0,0 +1,150 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <GeomHash_CurveHasher.hxx>
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Curve.hxx>
+#include <Geom_Line.hxx>
+#include <Geom_Circle.hxx>
+#include <Geom_Ellipse.hxx>
+#include <Geom_Hyperbola.hxx>
+#include <Geom_Parabola.hxx>
+#include <Geom_BezierCurve.hxx>
+#include <Geom_BSplineCurve.hxx>
+#include <Geom_TrimmedCurve.hxx>
+#include <Geom_OffsetCurve.hxx>
+
+#include <GeomHash_LineHasher.pxx>
+#include <GeomHash_CircleHasher.pxx>
+#include <GeomHash_EllipseHasher.pxx>
+#include <GeomHash_HyperbolaHasher.pxx>
+#include <GeomHash_ParabolaHasher.pxx>
+#include <GeomHash_BezierCurveHasher.pxx>
+#include <GeomHash_BSplineCurveHasher.pxx>
+#include <GeomHash_TrimmedCurveHasher.pxx>
+#include <GeomHash_OffsetCurveHasher.pxx>
+
+//=================================================================================================
+
+std::size_t GeomHash_CurveHasher::operator()(const Handle(Geom_Curve)& theCurve) const noexcept
+{
+  if (theCurve.IsNull())
+  {
+    return 0;
+  }
+
+  // Dispatch based on actual curve type
+  if (Handle(Geom_Line) aLine = Handle(Geom_Line)::DownCast(theCurve))
+  {
+    return GeomHash_LineHasher{}(aLine);
+  }
+  if (Handle(Geom_Circle) aCircle = Handle(Geom_Circle)::DownCast(theCurve))
+  {
+    return GeomHash_CircleHasher{}(aCircle);
+  }
+  if (Handle(Geom_Ellipse) anEllipse = Handle(Geom_Ellipse)::DownCast(theCurve))
+  {
+    return GeomHash_EllipseHasher{}(anEllipse);
+  }
+  if (Handle(Geom_Hyperbola) aHyperbola = Handle(Geom_Hyperbola)::DownCast(theCurve))
+  {
+    return GeomHash_HyperbolaHasher{}(aHyperbola);
+  }
+  if (Handle(Geom_Parabola) aParabola = Handle(Geom_Parabola)::DownCast(theCurve))
+  {
+    return GeomHash_ParabolaHasher{}(aParabola);
+  }
+  if (Handle(Geom_BezierCurve) aBezier = Handle(Geom_BezierCurve)::DownCast(theCurve))
+  {
+    return GeomHash_BezierCurveHasher{}(aBezier);
+  }
+  if (Handle(Geom_BSplineCurve) aBSpline = Handle(Geom_BSplineCurve)::DownCast(theCurve))
+  {
+    return GeomHash_BSplineCurveHasher{}(aBSpline);
+  }
+  if (Handle(Geom_TrimmedCurve) aTrimmed = Handle(Geom_TrimmedCurve)::DownCast(theCurve))
+  {
+    return GeomHash_TrimmedCurveHasher{}(aTrimmed);
+  }
+  if (Handle(Geom_OffsetCurve) anOffset = Handle(Geom_OffsetCurve)::DownCast(theCurve))
+  {
+    return GeomHash_OffsetCurveHasher{}(anOffset);
+  }
+
+  // Unknown curve type - hash the type name
+  return std::hash<std::string>{}(theCurve->DynamicType()->Name());
+}
+
+//=================================================================================================
+
+bool GeomHash_CurveHasher::operator()(const Handle(Geom_Curve)& theCurve1,
+                                      const Handle(Geom_Curve)& theCurve2) const noexcept
+{
+  if (theCurve1.IsNull() || theCurve2.IsNull())
+  {
+    return theCurve1.IsNull() && theCurve2.IsNull();
+  }
+
+  if (theCurve1 == theCurve2)
+  {
+    return true;
+  }
+
+  // Must be same type
+  if (theCurve1->DynamicType() != theCurve2->DynamicType())
+  {
+    return false;
+  }
+
+  // Dispatch based on actual curve type
+  if (Handle(Geom_Line) aLine1 = Handle(Geom_Line)::DownCast(theCurve1))
+  {
+    return GeomHash_LineHasher{}(aLine1, Handle(Geom_Line)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_Circle) aCircle1 = Handle(Geom_Circle)::DownCast(theCurve1))
+  {
+    return GeomHash_CircleHasher{}(aCircle1, Handle(Geom_Circle)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_Ellipse) anEllipse1 = Handle(Geom_Ellipse)::DownCast(theCurve1))
+  {
+    return GeomHash_EllipseHasher{}(anEllipse1, Handle(Geom_Ellipse)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_Hyperbola) aHyp1 = Handle(Geom_Hyperbola)::DownCast(theCurve1))
+  {
+    return GeomHash_HyperbolaHasher{}(aHyp1, Handle(Geom_Hyperbola)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_Parabola) aPar1 = Handle(Geom_Parabola)::DownCast(theCurve1))
+  {
+    return GeomHash_ParabolaHasher{}(aPar1, Handle(Geom_Parabola)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_BezierCurve) aBez1 = Handle(Geom_BezierCurve)::DownCast(theCurve1))
+  {
+    return GeomHash_BezierCurveHasher{}(aBez1, Handle(Geom_BezierCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_BSplineCurve) aBSpl1 = Handle(Geom_BSplineCurve)::DownCast(theCurve1))
+  {
+    return GeomHash_BSplineCurveHasher{}(aBSpl1, Handle(Geom_BSplineCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_TrimmedCurve) aTrim1 = Handle(Geom_TrimmedCurve)::DownCast(theCurve1))
+  {
+    return GeomHash_TrimmedCurveHasher{}(aTrim1, Handle(Geom_TrimmedCurve)::DownCast(theCurve2));
+  }
+  if (Handle(Geom_OffsetCurve) aOff1 = Handle(Geom_OffsetCurve)::DownCast(theCurve1))
+  {
+    return GeomHash_OffsetCurveHasher{}(aOff1, Handle(Geom_OffsetCurve)::DownCast(theCurve2));
+  }
+
+  // Unknown curve type - compare by pointer
+  return theCurve1.get() == theCurve2.get();
+}
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.hxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_CurveHasher.hxx
new file mode 100644 (file)
index 0000000..9036541
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_CurveHasher_HeaderFile
+#define _GeomHash_CurveHasher_HeaderFile
+
+#include <Standard_Handle.hxx>
+#include <cstddef>
+
+class Geom_Curve;
+
+//! Polymorphic hasher for Geom_Curve using RTTI dispatch.
+//! Used for geometry deduplication.
+struct GeomHash_CurveHasher
+{
+  // Hashes any Geom_Curve by dispatching to the appropriate specific hasher.
+  Standard_EXPORT std::size_t operator()(const Handle(Geom_Curve)& theCurve) const noexcept;
+
+  // Compares two curves using polymorphic dispatch.
+  Standard_EXPORT bool operator()(const Handle(Geom_Curve)& theCurve1,
+                                  const Handle(Geom_Curve)& theCurve2) const noexcept;
+};
+
+#endif // _GeomHash_CurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_CylindricalSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_CylindricalSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..5367df7
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_CylindricalSurfaceHasher_HeaderFile
+#define _GeomHash_CylindricalSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_CylindricalSurface.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_CylindricalSurface.
+//! Used for geometry deduplication.
+struct GeomHash_CylindricalSurfaceHasher
+{
+  // Hashes the cylinder by its position and radius.
+  std::size_t operator()(const Handle(Geom_CylindricalSurface)& theCylinder) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[2] = {
+      anAxisHasher(theCylinder->Position().Ax2()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCylinder->Radius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two cylinders by their positions and radii.
+  bool operator()(const Handle(Geom_CylindricalSurface)& theCylinder1,
+                  const Handle(Geom_CylindricalSurface)& theCylinder2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(theCylinder1->Position().Ax2(), theCylinder2->Position().Ax2())
+           && std::abs(theCylinder1->Radius() - theCylinder2->Radius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_CylindricalSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_DirectionHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_DirectionHasher.pxx
new file mode 100644 (file)
index 0000000..561b0e6
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_DirectionHasher_HeaderFile
+#define _GeomHash_DirectionHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Dir.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for gp_Dir (3D directions).
+//! Used for geometry deduplication.
+struct GeomHash_DirectionHasher
+{
+  // Hashes the 3D direction by its XYZ components.
+  std::size_t operator()(const gp_Dir& theDirection) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    // Round each component to tolerance precision before hashing
+    const std::size_t aHashes[3] = {
+      opencascade::hash(static_cast<int64_t>(std::round(theDirection.X() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theDirection.Y() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theDirection.Z() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 3D directions with fixed tolerance.
+  bool operator()(const gp_Dir& theDirection1, const gp_Dir& theDirection2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    return std::abs(theDirection1.X() - theDirection2.X()) <= aTolerance
+           && std::abs(theDirection1.Y() - theDirection2.Y()) <= aTolerance
+           && std::abs(theDirection1.Z() - theDirection2.Z()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_DirectionHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_EllipseHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_EllipseHasher.pxx
new file mode 100644 (file)
index 0000000..65c67da
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_EllipseHasher_HeaderFile
+#define _GeomHash_EllipseHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Ellipse.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_Ellipse (3D ellipse).
+//! Used for geometry deduplication.
+struct GeomHash_EllipseHasher
+{
+  // Hashes the ellipse by its position, major radius, and minor radius.
+  std::size_t operator()(const Handle(Geom_Ellipse)& theEllipse) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[3] = {
+      anAxisHasher(theEllipse->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theEllipse->MajorRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theEllipse->MinorRadius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two ellipses by their positions and radii.
+  bool operator()(const Handle(Geom_Ellipse)& theEllipse1,
+                  const Handle(Geom_Ellipse)& theEllipse2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theEllipse1->Position(), theEllipse2->Position())
+           && std::abs(theEllipse1->MajorRadius() - theEllipse2->MajorRadius()) <= aTolerance
+           && std::abs(theEllipse1->MinorRadius() - theEllipse2->MinorRadius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_EllipseHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_HyperbolaHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_HyperbolaHasher.pxx
new file mode 100644 (file)
index 0000000..68800d8
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_HyperbolaHasher_HeaderFile
+#define _GeomHash_HyperbolaHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Hyperbola.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_Hyperbola (3D hyperbola).
+//! Used for geometry deduplication.
+struct GeomHash_HyperbolaHasher
+{
+  // Hashes the hyperbola by its position, major radius, and minor radius.
+  std::size_t operator()(const Handle(Geom_Hyperbola)& theHyperbola) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[3] = {
+      anAxisHasher(theHyperbola->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theHyperbola->MajorRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theHyperbola->MinorRadius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two hyperbolas by their positions and radii.
+  bool operator()(const Handle(Geom_Hyperbola)& theHyperbola1,
+                  const Handle(Geom_Hyperbola)& theHyperbola2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theHyperbola1->Position(), theHyperbola2->Position())
+           && std::abs(theHyperbola1->MajorRadius() - theHyperbola2->MajorRadius()) <= aTolerance
+           && std::abs(theHyperbola1->MinorRadius() - theHyperbola2->MinorRadius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_HyperbolaHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_LineHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_LineHasher.pxx
new file mode 100644 (file)
index 0000000..a6ff887
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_LineHasher_HeaderFile
+#define _GeomHash_LineHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Line.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <GeomHash_DirectionHasher.pxx>
+
+//! OCCT-style hasher for Geom_Line (3D line).
+//! Used for geometry deduplication.
+struct GeomHash_LineHasher
+{
+  // Hashes the line by its location and direction.
+  std::size_t operator()(const Handle(Geom_Line)& theLine) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirHasher;
+
+    const std::size_t aHashes[2] = {aPointHasher(theLine->Position().Location()),
+                                    aDirHasher(theLine->Position().Direction())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two lines by their positions.
+  bool operator()(const Handle(Geom_Line)& theLine1,
+                  const Handle(Geom_Line)& theLine2) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirHasher;
+
+    return aPointHasher(theLine1->Position().Location(), theLine2->Position().Location())
+           && aDirHasher(theLine1->Position().Direction(), theLine2->Position().Direction());
+  }
+};
+
+#endif // _GeomHash_LineHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetCurveHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetCurveHasher.pxx
new file mode 100644 (file)
index 0000000..5c16d15
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_OffsetCurveHasher_HeaderFile
+#define _GeomHash_OffsetCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_OffsetCurve.hxx>
+#include <GeomHash_DirectionHasher.pxx>
+#include <GeomHash_CurveHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_OffsetCurve (3D offset curve).
+//! Used for geometry deduplication.
+struct GeomHash_OffsetCurveHasher
+{
+  // Hashes the offset curve by its offset distance, direction, and basis curve.
+  std::size_t operator()(const Handle(Geom_OffsetCurve)& theCurve) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+    const std::size_t              aHashes[3] = {
+      aCurveHasher(theCurve->BasisCurve()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->Offset() * aFactor))),
+      aDirHasher(theCurve->Direction())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two offset curves.
+  bool operator()(const Handle(Geom_OffsetCurve)& theCurve1,
+                  const Handle(Geom_OffsetCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+
+    return aCurveHasher(theCurve1->BasisCurve(), theCurve2->BasisCurve())
+           && std::abs(theCurve1->Offset() - theCurve2->Offset()) <= aTolerance
+           && aDirHasher(theCurve1->Direction(), theCurve2->Direction());
+  }
+};
+
+#endif // _GeomHash_OffsetCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_OffsetSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..a6b95a8
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_OffsetSurfaceHasher_HeaderFile
+#define _GeomHash_OffsetSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_OffsetSurface.hxx>
+#include <GeomHash_SurfaceHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_OffsetSurface.
+//! Used for geometry deduplication.
+struct GeomHash_OffsetSurfaceHasher
+{
+  // Hashes the offset surface by its offset distance and basis surface.
+  std::size_t operator()(const Handle(Geom_OffsetSurface)& theSurface) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_SurfaceHasher aSurfaceHasher;
+    const std::size_t            aHashes[2] = {
+      aSurfaceHasher(theSurface->BasisSurface()),
+      opencascade::hash(static_cast<int64_t>(std::round(theSurface->Offset() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two offset surfaces by their offset distances and basis surfaces.
+  bool operator()(const Handle(Geom_OffsetSurface)& theSurface1,
+                  const Handle(Geom_OffsetSurface)& theSurface2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const GeomHash_SurfaceHasher aSurfaceHasher;
+
+    return aSurfaceHasher(theSurface1->BasisSurface(), theSurface2->BasisSurface())
+           && std::abs(theSurface1->Offset() - theSurface2->Offset()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_OffsetSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_ParabolaHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_ParabolaHasher.pxx
new file mode 100644 (file)
index 0000000..073cf87
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_ParabolaHasher_HeaderFile
+#define _GeomHash_ParabolaHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Parabola.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_Parabola (3D parabola).
+//! Used for geometry deduplication.
+struct GeomHash_ParabolaHasher
+{
+  // Hashes the parabola by its position and focal length.
+  std::size_t operator()(const Handle(Geom_Parabola)& theParabola) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[2] = {
+      anAxisHasher(theParabola->Position()),
+      opencascade::hash(static_cast<int64_t>(std::round(theParabola->Focal() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two parabolas by their positions and focal lengths.
+  bool operator()(const Handle(Geom_Parabola)& theParabola1,
+                  const Handle(Geom_Parabola)& theParabola2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+
+    return anAxisHasher(theParabola1->Position(), theParabola2->Position())
+           && std::abs(theParabola1->Focal() - theParabola2->Focal()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_ParabolaHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_PlaneHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_PlaneHasher.pxx
new file mode 100644 (file)
index 0000000..6aeeb56
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_PlaneHasher_HeaderFile
+#define _GeomHash_PlaneHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Plane.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+
+//! OCCT-style hasher for Geom_Plane surfaces.
+//! Used for geometry deduplication.
+struct GeomHash_PlaneHasher
+{
+  // Hashes the plane by its position (location, normal, reference direction).
+  std::size_t operator()(const Handle(Geom_Plane)& thePlane) const noexcept
+  {
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(thePlane->Position().Ax2());
+  }
+
+  // Compares two planes by their positions.
+  bool operator()(const Handle(Geom_Plane)& thePlane1,
+                  const Handle(Geom_Plane)& thePlane2) const noexcept
+  {
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(thePlane1->Position().Ax2(), thePlane2->Position().Ax2());
+  }
+};
+
+#endif // _GeomHash_PlaneHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_PointHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_PointHasher.pxx
new file mode 100644 (file)
index 0000000..d7d9cf0
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_PointHasher_HeaderFile
+#define _GeomHash_PointHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Pnt.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for gp_Pnt (3D points).
+//! Used for geometry deduplication.
+struct GeomHash_PointHasher
+{
+  // Hashes the 3D point by its XYZ coordinates.
+  std::size_t operator()(const gp_Pnt& thePoint) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    // Round each coordinate to tolerance precision before hashing
+    const std::size_t aHashes[3] = {
+      opencascade::hash(static_cast<int64_t>(std::round(thePoint.X() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(thePoint.Y() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(thePoint.Z() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 3D points with fixed tolerance.
+  bool operator()(const gp_Pnt& thePoint1, const gp_Pnt& thePoint2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    return std::abs(thePoint1.X() - thePoint2.X()) <= aTolerance
+           && std::abs(thePoint1.Y() - thePoint2.Y()) <= aTolerance
+           && std::abs(thePoint1.Z() - thePoint2.Z()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_PointHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_RectangularTrimmedSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_RectangularTrimmedSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..774e0cc
--- /dev/null
@@ -0,0 +1,71 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_RectangularTrimmedSurfaceHasher_HeaderFile
+#define _GeomHash_RectangularTrimmedSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_RectangularTrimmedSurface.hxx>
+#include <GeomHash_SurfaceHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_RectangularTrimmedSurface.
+//! Used for geometry deduplication.
+struct GeomHash_RectangularTrimmedSurfaceHasher
+{
+  // Hashes the trimmed surface by its trim bounds and basis surface.
+  std::size_t operator()(const Handle(Geom_RectangularTrimmedSurface)& theSurface) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_SurfaceHasher aSurfaceHasher;
+
+    double aU1, aU2, aV1, aV2;
+    theSurface->Bounds(aU1, aU2, aV1, aV2);
+
+    const std::size_t aHashes[5] = {
+      aSurfaceHasher(theSurface->BasisSurface()),
+      opencascade::hash(static_cast<int64_t>(std::round(aU1 * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(aU2 * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(aV1 * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(aV2 * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two trimmed surfaces by their trim bounds and basis surfaces.
+  bool operator()(const Handle(Geom_RectangularTrimmedSurface)& theSurface1,
+                  const Handle(Geom_RectangularTrimmedSurface)& theSurface2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const GeomHash_SurfaceHasher aSurfaceHasher;
+
+    // Compare basis surfaces
+    if (!aSurfaceHasher(theSurface1->BasisSurface(), theSurface2->BasisSurface()))
+    {
+      return false;
+    }
+
+    // Compare trim bounds
+    double aU1_1, aU2_1, aV1_1, aV2_1;
+    double aU1_2, aU2_2, aV1_2, aV2_2;
+    theSurface1->Bounds(aU1_1, aU2_1, aV1_1, aV2_1);
+    theSurface2->Bounds(aU1_2, aU2_2, aV1_2, aV2_2);
+
+    return std::abs(aU1_1 - aU1_2) <= aTolerance && std::abs(aU2_1 - aU2_2) <= aTolerance
+           && std::abs(aV1_1 - aV1_2) <= aTolerance && std::abs(aV2_1 - aV2_2) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_RectangularTrimmedSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_SphericalSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_SphericalSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..06a71c1
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_SphericalSurfaceHasher_HeaderFile
+#define _GeomHash_SphericalSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_SphericalSurface.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_SphericalSurface.
+//! Used for geometry deduplication.
+struct GeomHash_SphericalSurfaceHasher
+{
+  // Hashes the sphere by its position and radius.
+  std::size_t operator()(const Handle(Geom_SphericalSurface)& theSphere) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[2] = {
+      anAxisHasher(theSphere->Position().Ax2()),
+      opencascade::hash(static_cast<int64_t>(std::round(theSphere->Radius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two spheres by their positions and radii.
+  bool operator()(const Handle(Geom_SphericalSurface)& theSphere1,
+                  const Handle(Geom_SphericalSurface)& theSphere2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(theSphere1->Position().Ax2(), theSphere2->Position().Ax2())
+           && std::abs(theSphere1->Radius() - theSphere2->Radius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_SphericalSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.cxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.cxx
new file mode 100644 (file)
index 0000000..01fa9db
--- /dev/null
@@ -0,0 +1,191 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#include <GeomHash_SurfaceHasher.hxx>
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_Surface.hxx>
+#include <Geom_Plane.hxx>
+#include <Geom_CylindricalSurface.hxx>
+#include <Geom_ConicalSurface.hxx>
+#include <Geom_SphericalSurface.hxx>
+#include <Geom_ToroidalSurface.hxx>
+#include <Geom_SurfaceOfRevolution.hxx>
+#include <Geom_SurfaceOfLinearExtrusion.hxx>
+#include <Geom_BezierSurface.hxx>
+#include <Geom_BSplineSurface.hxx>
+#include <Geom_RectangularTrimmedSurface.hxx>
+#include <Geom_OffsetSurface.hxx>
+
+#include <GeomHash_PlaneHasher.pxx>
+#include <GeomHash_CylindricalSurfaceHasher.pxx>
+#include <GeomHash_ConicalSurfaceHasher.pxx>
+#include <GeomHash_SphericalSurfaceHasher.pxx>
+#include <GeomHash_ToroidalSurfaceHasher.pxx>
+#include <GeomHash_SurfaceOfRevolutionHasher.pxx>
+#include <GeomHash_SurfaceOfLinearExtrusionHasher.pxx>
+#include <GeomHash_BezierSurfaceHasher.pxx>
+#include <GeomHash_BSplineSurfaceHasher.pxx>
+#include <GeomHash_RectangularTrimmedSurfaceHasher.pxx>
+#include <GeomHash_OffsetSurfaceHasher.pxx>
+
+//=================================================================================================
+
+std::size_t GeomHash_SurfaceHasher::operator()(
+  const Handle(Geom_Surface)& theSurface) const noexcept
+{
+  if (theSurface.IsNull())
+  {
+    return 0;
+  }
+
+  // Dispatch based on actual surface type
+  if (Handle(Geom_Plane) aPlane = Handle(Geom_Plane)::DownCast(theSurface))
+  {
+    return GeomHash_PlaneHasher{}(aPlane);
+  }
+  if (Handle(Geom_CylindricalSurface) aCylinder =
+        Handle(Geom_CylindricalSurface)::DownCast(theSurface))
+  {
+    return GeomHash_CylindricalSurfaceHasher{}(aCylinder);
+  }
+  if (Handle(Geom_ConicalSurface) aCone = Handle(Geom_ConicalSurface)::DownCast(theSurface))
+  {
+    return GeomHash_ConicalSurfaceHasher{}(aCone);
+  }
+  if (Handle(Geom_SphericalSurface) aSphere = Handle(Geom_SphericalSurface)::DownCast(theSurface))
+  {
+    return GeomHash_SphericalSurfaceHasher{}(aSphere);
+  }
+  if (Handle(Geom_ToroidalSurface) aTorus = Handle(Geom_ToroidalSurface)::DownCast(theSurface))
+  {
+    return GeomHash_ToroidalSurfaceHasher{}(aTorus);
+  }
+  if (Handle(Geom_SurfaceOfRevolution) aRevol =
+        Handle(Geom_SurfaceOfRevolution)::DownCast(theSurface))
+  {
+    return GeomHash_SurfaceOfRevolutionHasher{}(aRevol);
+  }
+  if (Handle(Geom_SurfaceOfLinearExtrusion) aExtr =
+        Handle(Geom_SurfaceOfLinearExtrusion)::DownCast(theSurface))
+  {
+    return GeomHash_SurfaceOfLinearExtrusionHasher{}(aExtr);
+  }
+  if (Handle(Geom_BezierSurface) aBezier = Handle(Geom_BezierSurface)::DownCast(theSurface))
+  {
+    return GeomHash_BezierSurfaceHasher{}(aBezier);
+  }
+  if (Handle(Geom_BSplineSurface) aBSpline = Handle(Geom_BSplineSurface)::DownCast(theSurface))
+  {
+    return GeomHash_BSplineSurfaceHasher{}(aBSpline);
+  }
+  if (Handle(Geom_RectangularTrimmedSurface) aTrimmed =
+        Handle(Geom_RectangularTrimmedSurface)::DownCast(theSurface))
+  {
+    return GeomHash_RectangularTrimmedSurfaceHasher{}(aTrimmed);
+  }
+  if (Handle(Geom_OffsetSurface) aOffset = Handle(Geom_OffsetSurface)::DownCast(theSurface))
+  {
+    return GeomHash_OffsetSurfaceHasher{}(aOffset);
+  }
+
+  // Unknown surface type - hash the type name
+  return std::hash<std::string>{}(theSurface->DynamicType()->Name());
+}
+
+//=================================================================================================
+
+bool GeomHash_SurfaceHasher::operator()(const Handle(Geom_Surface)& theSurface1,
+                                        const Handle(Geom_Surface)& theSurface2) const noexcept
+{
+  if (theSurface1.IsNull() || theSurface2.IsNull())
+  {
+    return theSurface1.IsNull() && theSurface2.IsNull();
+  }
+
+  if (theSurface1 == theSurface2)
+  {
+    return true;
+  }
+
+  // Must be same type
+  if (theSurface1->DynamicType() != theSurface2->DynamicType())
+  {
+    return false;
+  }
+
+  // Dispatch based on actual surface type
+  if (Handle(Geom_Plane) aPlane1 = Handle(Geom_Plane)::DownCast(theSurface1))
+  {
+    return GeomHash_PlaneHasher{}(aPlane1, Handle(Geom_Plane)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_CylindricalSurface) aCyl1 =
+        Handle(Geom_CylindricalSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_CylindricalSurfaceHasher{}(
+      aCyl1,
+      Handle(Geom_CylindricalSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_ConicalSurface) aCone1 = Handle(Geom_ConicalSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_ConicalSurfaceHasher{}(aCone1,
+                                           Handle(Geom_ConicalSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_SphericalSurface) aSph1 = Handle(Geom_SphericalSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_SphericalSurfaceHasher{}(aSph1,
+                                             Handle(Geom_SphericalSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_ToroidalSurface) aTor1 = Handle(Geom_ToroidalSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_ToroidalSurfaceHasher{}(aTor1,
+                                            Handle(Geom_ToroidalSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_SurfaceOfRevolution) aRev1 =
+        Handle(Geom_SurfaceOfRevolution)::DownCast(theSurface1))
+  {
+    return GeomHash_SurfaceOfRevolutionHasher{}(
+      aRev1,
+      Handle(Geom_SurfaceOfRevolution)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_SurfaceOfLinearExtrusion) aExt1 =
+        Handle(Geom_SurfaceOfLinearExtrusion)::DownCast(theSurface1))
+  {
+    return GeomHash_SurfaceOfLinearExtrusionHasher{}(
+      aExt1,
+      Handle(Geom_SurfaceOfLinearExtrusion)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_BezierSurface) aBez1 = Handle(Geom_BezierSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_BezierSurfaceHasher{}(aBez1, Handle(Geom_BezierSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_BSplineSurface) aBSpl1 = Handle(Geom_BSplineSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_BSplineSurfaceHasher{}(aBSpl1,
+                                           Handle(Geom_BSplineSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_RectangularTrimmedSurface) aTrim1 =
+        Handle(Geom_RectangularTrimmedSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_RectangularTrimmedSurfaceHasher{}(
+      aTrim1,
+      Handle(Geom_RectangularTrimmedSurface)::DownCast(theSurface2));
+  }
+  if (Handle(Geom_OffsetSurface) aOff1 = Handle(Geom_OffsetSurface)::DownCast(theSurface1))
+  {
+    return GeomHash_OffsetSurfaceHasher{}(aOff1, Handle(Geom_OffsetSurface)::DownCast(theSurface2));
+  }
+
+  // Unknown surface type - compare by pointer
+  return theSurface1.get() == theSurface2.get();
+}
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.hxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceHasher.hxx
new file mode 100644 (file)
index 0000000..f4cd74b
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_SurfaceHasher_HeaderFile
+#define _GeomHash_SurfaceHasher_HeaderFile
+
+#include <Standard_Handle.hxx>
+#include <cstddef>
+
+class Geom_Surface;
+
+//! Polymorphic hasher for Geom_Surface using RTTI dispatch.
+//! Used for geometry deduplication.
+struct GeomHash_SurfaceHasher
+{
+  // Hashes any Geom_Surface by dispatching to the appropriate specific hasher.
+  Standard_EXPORT std::size_t operator()(const Handle(Geom_Surface)& theSurface) const noexcept;
+
+  // Compares two surfaces using polymorphic dispatch.
+  Standard_EXPORT bool operator()(const Handle(Geom_Surface)& theSurface1,
+                                  const Handle(Geom_Surface)& theSurface2) const noexcept;
+};
+
+#endif // _GeomHash_SurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfLinearExtrusionHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfLinearExtrusionHasher.pxx
new file mode 100644 (file)
index 0000000..b1d2fba
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_SurfaceOfLinearExtrusionHasher_HeaderFile
+#define _GeomHash_SurfaceOfLinearExtrusionHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_SurfaceOfLinearExtrusion.hxx>
+#include <GeomHash_DirectionHasher.pxx>
+#include <GeomHash_CurveHasher.hxx>
+
+//! OCCT-style hasher for Geom_SurfaceOfLinearExtrusion.
+//! Used for geometry deduplication.
+struct GeomHash_SurfaceOfLinearExtrusionHasher
+{
+  // Hashes the extrusion surface by its direction and basis curve.
+  std::size_t operator()(const Handle(Geom_SurfaceOfLinearExtrusion)& theSurface) const noexcept
+  {
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+    const std::size_t              aHashes[2] = {aCurveHasher(theSurface->BasisCurve()),
+                                                 aDirHasher(theSurface->Direction())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two extrusion surfaces.
+  bool operator()(const Handle(Geom_SurfaceOfLinearExtrusion)& theSurface1,
+                  const Handle(Geom_SurfaceOfLinearExtrusion)& theSurface2) const noexcept
+  {
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+
+    return aCurveHasher(theSurface1->BasisCurve(), theSurface2->BasisCurve())
+           && aDirHasher(theSurface1->Direction(), theSurface2->Direction());
+  }
+};
+
+#endif // _GeomHash_SurfaceOfLinearExtrusionHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfRevolutionHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_SurfaceOfRevolutionHasher.pxx
new file mode 100644 (file)
index 0000000..ca3a899
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_SurfaceOfRevolutionHasher_HeaderFile
+#define _GeomHash_SurfaceOfRevolutionHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_SurfaceOfRevolution.hxx>
+#include <GeomHash_PointHasher.pxx>
+#include <GeomHash_DirectionHasher.pxx>
+#include <GeomHash_CurveHasher.hxx>
+
+//! OCCT-style hasher for Geom_SurfaceOfRevolution.
+//! Used for geometry deduplication.
+struct GeomHash_SurfaceOfRevolutionHasher
+{
+  // Hashes the revolution surface by its axis and basis curve.
+  std::size_t operator()(const Handle(Geom_SurfaceOfRevolution)& theSurface) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+
+    const gp_Ax1&     anAxis     = theSurface->Axis();
+    const std::size_t aHashes[3] = {aCurveHasher(theSurface->BasisCurve()),
+                                    aPointHasher(anAxis.Location()),
+                                    aDirHasher(anAxis.Direction())};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two revolution surfaces.
+  bool operator()(const Handle(Geom_SurfaceOfRevolution)& theSurface1,
+                  const Handle(Geom_SurfaceOfRevolution)& theSurface2) const noexcept
+  {
+    const GeomHash_PointHasher     aPointHasher;
+    const GeomHash_DirectionHasher aDirHasher;
+    const GeomHash_CurveHasher     aCurveHasher;
+
+    const gp_Ax1& anAxis1 = theSurface1->Axis();
+    const gp_Ax1& anAxis2 = theSurface2->Axis();
+
+    return aCurveHasher(theSurface1->BasisCurve(), theSurface2->BasisCurve())
+           && aPointHasher(anAxis1.Location(), anAxis2.Location())
+           && aDirHasher(anAxis1.Direction(), anAxis2.Direction());
+  }
+};
+
+#endif // _GeomHash_SurfaceOfRevolutionHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_ToroidalSurfaceHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_ToroidalSurfaceHasher.pxx
new file mode 100644 (file)
index 0000000..ec6558e
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_ToroidalSurfaceHasher_HeaderFile
+#define _GeomHash_ToroidalSurfaceHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_ToroidalSurface.hxx>
+#include <GeomHash_AxisPlacement.pxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_ToroidalSurface.
+//! Used for geometry deduplication.
+struct GeomHash_ToroidalSurfaceHasher
+{
+  // Hashes the torus by its position, major radius, and minor radius.
+  std::size_t operator()(const Handle(Geom_ToroidalSurface)& theTorus) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_AxisPlacement anAxisHasher;
+    const std::size_t            aHashes[3] = {
+      anAxisHasher(theTorus->Position().Ax2()),
+      opencascade::hash(static_cast<int64_t>(std::round(theTorus->MajorRadius() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theTorus->MinorRadius() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two tori by their positions, major radii, and minor radii.
+  bool operator()(const Handle(Geom_ToroidalSurface)& theTorus1,
+                  const Handle(Geom_ToroidalSurface)& theTorus2) const noexcept
+  {
+    constexpr double             aTolerance = 1e-12;
+    const GeomHash_AxisPlacement anAxisHasher;
+    return anAxisHasher(theTorus1->Position().Ax2(), theTorus2->Position().Ax2())
+           && std::abs(theTorus1->MajorRadius() - theTorus2->MajorRadius()) <= aTolerance
+           && std::abs(theTorus1->MinorRadius() - theTorus2->MinorRadius()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_ToroidalSurfaceHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_TrimmedCurveHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_TrimmedCurveHasher.pxx
new file mode 100644 (file)
index 0000000..f6e17cd
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_TrimmedCurveHasher_HeaderFile
+#define _GeomHash_TrimmedCurveHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <Geom_TrimmedCurve.hxx>
+#include <GeomHash_CurveHasher.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for Geom_TrimmedCurve (3D trimmed curve).
+//! Used for geometry deduplication.
+struct GeomHash_TrimmedCurveHasher
+{
+  // Hashes the trimmed curve by its parameters and basis curve.
+  std::size_t operator()(const Handle(Geom_TrimmedCurve)& theCurve) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    const GeomHash_CurveHasher aCurveHasher;
+    const std::size_t          aHashes[3] = {
+      aCurveHasher(theCurve->BasisCurve()),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->FirstParameter() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theCurve->LastParameter() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two trimmed curves.
+  bool operator()(const Handle(Geom_TrimmedCurve)& theCurve1,
+                  const Handle(Geom_TrimmedCurve)& theCurve2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+
+    const GeomHash_CurveHasher aCurveHasher;
+
+    return aCurveHasher(theCurve1->BasisCurve(), theCurve2->BasisCurve())
+           && std::abs(theCurve1->FirstParameter() - theCurve2->FirstParameter()) <= aTolerance
+           && std::abs(theCurve1->LastParameter() - theCurve2->LastParameter()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_TrimmedCurveHasher_HeaderFile
diff --git a/src/ModelingData/TKG3d/GeomHash/GeomHash_VectorHasher.pxx b/src/ModelingData/TKG3d/GeomHash/GeomHash_VectorHasher.pxx
new file mode 100644 (file)
index 0000000..b81f0eb
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 OPEN CASCADE SAS
+//
+// This file is part of Open CASCADE Technology software library.
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License version 2.1 as published
+// by the Free Software Foundation, with special exception defined in the file
+// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
+// distribution for complete text of the license and disclaimer of any warranty.
+//
+// Alternatively, this file may be used under the terms of Open CASCADE
+// commercial license or contractual agreement.
+
+#ifndef _GeomHash_VectorHasher_HeaderFile
+#define _GeomHash_VectorHasher_HeaderFile
+
+#include <Standard_HashUtils.hxx>
+#include <gp_Vec.hxx>
+#include <cmath>
+
+//! OCCT-style hasher for gp_Vec (3D vectors).
+//! Used for geometry deduplication.
+struct GeomHash_VectorHasher
+{
+  // Hashes the 3D vector by its XYZ components.
+  std::size_t operator()(const gp_Vec& theVector) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    constexpr double aFactor    = 1.0 / aTolerance;
+
+    // Round each component to tolerance precision before hashing
+    const std::size_t aHashes[3] = {
+      opencascade::hash(static_cast<int64_t>(std::round(theVector.X() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theVector.Y() * aFactor))),
+      opencascade::hash(static_cast<int64_t>(std::round(theVector.Z() * aFactor)))};
+    return opencascade::hashBytes(aHashes, sizeof(aHashes));
+  }
+
+  // Compares two 3D vectors with fixed tolerance.
+  bool operator()(const gp_Vec& theVector1, const gp_Vec& theVector2) const noexcept
+  {
+    constexpr double aTolerance = 1e-12;
+    return std::abs(theVector1.X() - theVector2.X()) <= aTolerance
+           && std::abs(theVector1.Y() - theVector2.Y()) <= aTolerance
+           && std::abs(theVector1.Z() - theVector2.Z()) <= aTolerance;
+  }
+};
+
+#endif // _GeomHash_VectorHasher_HeaderFile
index f7bad084e3da5c8c23e59b8431eb00c2a067e187..f3e1b7279170e0062215f33fa0d8dbfa70d4996e 100644 (file)
@@ -10,4 +10,5 @@ set(OCCT_TKG3d_LIST_OF_PACKAGES
   TopAbs
   GeomEvaluator
   GProp
+  GeomHash
 )