From 057057484d76abd4519a9a823c49ac542e520034 Mon Sep 17 00:00:00 2001 From: RanaPriyansh Date: Tue, 29 Sep 2026 15:13:06 +0530 Subject: [PATCH] fix: bound countersink cuts by explicit depth --- cadquery/cq.py | 5 +- tests/test_cadquery.py | 106 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 8d5a4cd25..5391e333c 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2913,6 +2913,7 @@ def cskHole( see :meth:`cboreHole` to make counterbores instead of countersinks """ + cskDepth = depth if depth is None: depth = self.largestDimension() @@ -2925,7 +2926,9 @@ def cskHole( ) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) - csk = Solid.makeCone(r, 0.0, h, center, boreDir) + csk: Shape = Solid.makeCone(r, 0.0, h, center, boreDir) + if cskDepth is not None and cskDepth < h: + csk = csk.intersect(Solid.makeCylinder(r, cskDepth, center, boreDir)) res = hole.fuse(csk) return self.cutEach(lambda loc: res.moved(loc), True, clean) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index a1de3c957..43b2af7c9 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -1546,6 +1546,112 @@ def testAngledHoles(self): self.saveModel(s) self.assertEqual(10, s.faces().size()) + def testCountersinkDepthBoundsCut(self): + """A specified hole depth also bounds the countersink cut.""" + entrance_radius = 4.0 + bore_radius = 1.0 + transition_depth = entrance_radius - bore_radius + transition_volume = ( + math.pi + * transition_depth + * (entrance_radius ** 2 + entrance_radius * bore_radius + bore_radius ** 2) + / 3 + ) + + for depth in (1.0, 3.5, 4.0 - 5e-8, 4.0, 8.0): + with self.subTest(depth=depth): + part = ( + Workplane("XY") + .box(20, 20, 10) + .faces(">Z") + .workplane() + .cskHole(2, 8, 90, depth) + .val() + ) + if depth < transition_depth: + end_radius = entrance_radius - depth + removed = ( + math.pi + * depth + * ( + entrance_radius ** 2 + + entrance_radius * end_radius + + end_radius ** 2 + ) + / 3 + ) + else: + removed = transition_volume + math.pi * bore_radius ** 2 * ( + depth - transition_depth + ) + + self.assertTrue(part.isValid()) + self.assertAlmostEqual(part.Volume(), 4000 - removed, places=6) + self.assertTrue(part.isInside(Vector(0, 0, 5 - depth - 0.1))) + + through_part = ( + Workplane("XY") + .box(20, 20, 10) + .faces(">Z") + .workplane() + .cskHole(2, 8, 90) + .val() + ) + through_removed = transition_volume + math.pi * bore_radius ** 2 * ( + 10 - transition_depth + ) + self.assertTrue(through_part.isValid()) + self.assertAlmostEqual(through_part.Volume(), 4000 - through_removed, places=6) + self.assertFalse(through_part.isInside(Vector(0, 0, -4.9))) + + def testCountersinkDepthPreservesBottomMaterial(self): + """A shallow angled countersink leaves material below the requested depth.""" + part = ( + Workplane("XY") + .cylinder(15, 30) + .faces(">Z") + .workplane() + .cskHole(56, 58, 45, 13) + .val() + ) + + self.assertTrue(part.isValid()) + self.assertTrue(part.isInside(Vector(0, 0, -7.4))) + + def testCountersinkNearEqualDiameters(self): + """A positive countersink rim remains valid when its radii are close.""" + entrance_radius = 4.0 + depth = 1.0 + + for radius_gap in (1e-8, 1e-3): + with self.subTest(radius_gap=radius_gap): + bore_radius = entrance_radius - radius_gap + part = ( + Workplane("XY") + .box(20, 20, 10) + .faces(">Z") + .workplane() + .cskHole(2 * bore_radius, 2 * entrance_radius, 90, depth) + .val() + ) + transition_volume = ( + math.pi + * radius_gap + * ( + entrance_radius ** 2 + + entrance_radius * bore_radius + + bore_radius ** 2 + ) + / 3 + ) + removed = transition_volume + math.pi * bore_radius ** 2 * ( + depth - radius_gap + ) + + self.assertTrue(part.isValid()) + self.assertAlmostEqual(part.Volume(), 4000 - removed, places=6) + self.assertTrue(part.isInside(Vector(0, 0, 3.9))) + def testTranslateSolid(self): c = CQ(makeUnitCube()) self.assertAlmostEqual(0.0, c.faces("