Skip to content

Commit fc0d5eb

Browse files
committed
Merge branch 'dev'
2 parents d52b0cd + 3a9d15e commit fc0d5eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1026
-467
lines changed

Package.resolved

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ let package = Package(
1010
.executable(name: "Helical-Demo", targets: ["Demo"]),
1111
],
1212
dependencies: [
13-
.package(url: "https://github.com/tomasf/Cadova.git", .upToNextMinor(from: "0.5.0")),
13+
.package(url: "https://github.com/tomasf/Cadova.git", .upToNextMinor(from: "0.5.2")),
1414
],
1515
targets: [
1616
.target(

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ let customBolt = Bolt(
6666
unthreadedLength: 3,
6767
unthreadedDiameter: 5,
6868
headShape: .countersunk(angle: 80°, topDiameter: 10, boltDiameter: 5),
69-
socket: .slotted(length: 10, width: 1, depth: 1.4)
69+
socket: .slot(length: 10, width: 1, depth: 1.4)
7070
)
7171
```
7272

@@ -80,7 +80,7 @@ Creating a matching countersunk clearance hole for a bolt is straightforward:
8080
Box(13)
8181
.aligned(at: .centerXY)
8282
.subtracting {
83-
customBolt.clearanceHole(recessedHead: true)
83+
customBolt.clearanceHole(entry: .recessedHead)
8484
}
8585
```
8686

Sources/Demo/main.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ let customBolt = Bolt(
99
thread: customThread,
1010
length: boltLength,
1111
unthreadedLength: 0,
12-
leadinChamferSize: 1.0,
1312
headShape: PolygonalBoltHeadShape(sideCount: 8, widthAcrossFlats: 12, height: 3, chamferAngle: 40°),
14-
socket: PolygonalBoltHeadSocket(sides: 4, acrossWidth: 5, depth: 2)
13+
socket: PolygonalBoltHeadSocket(sides: 4, width: 5, depth: 2),
14+
point: .leadIn(.constant(depth: 1))
1515
)
1616

1717
let bolts = [
@@ -35,7 +35,7 @@ let nutsAndWashers: [(String, any Geometry3D)] = [
3535
("Thin square nut, M10", Nut.square(.m10, series: .thin)),
3636
("Flanged hex nut, M6", Nut.flangedHex(.m6)),
3737
("T-slot nut, M8", Nut.tSlotNut(.m8)),
38-
("Custom non-standard nut", Nut(thread: customThread, shape: PolygonalNutBody(sideCount: 8, thickness: 10, widthAcrossFlats: 12), innerChamferAngle: 60°)),
38+
("Custom non-standard nut", Nut(thread: customThread, shape: PolygonalNutBody(sideCount: 8, thickness: 10, widthAcrossFlats: 12), leadIns: .both(.angle(60°)))),
3939
("Normal washer, M5", Washer.plain(.m5, series: .normal)),
4040
("Large washer, M5", Washer.plain(.m5, series: .large))
4141
]

Sources/Helical/Bolt/Bolt.swift

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import Foundation
21
import Cadova
32

4-
/// A bolt with a head, threaded section, and optional point.
3+
/// A complete bolt fastener with a head, threaded shank, and optional point.
4+
///
5+
/// `Bolt` composes a head shape, drive socket, threaded section (``Screw``), and point
6+
/// into a full fastener model. For just the bare threaded geometry, use ``Screw``.
57
public struct Bolt: Shape3D {
68
/// The screw thread specification.
79
public let thread: ScrewThread
@@ -46,36 +48,6 @@ public struct Bolt: Shape3D {
4648
self.point = point
4749
}
4850

49-
/// Creates a bolt with a chamfered point.
50-
///
51-
/// - Parameters:
52-
/// - thread: The screw thread specification.
53-
/// - length: Nominal length of the bolt.
54-
/// - unthreadedLength: Length of the unthreaded portion.
55-
/// - unthreadedDiameter: Diameter of the unthreaded portion. Defaults to the thread's major diameter.
56-
/// - leadinChamferSize: Size of the lead-in chamfer at the bolt tip.
57-
/// - headShape: The bolt head geometry.
58-
/// - socket: Optional drive socket in the head.
59-
public init(
60-
thread: ScrewThread,
61-
length: Double,
62-
unthreadedLength: Double = 0,
63-
unthreadedDiameter: Double? = nil,
64-
leadinChamferSize: Double,
65-
headShape: any BoltHeadShape,
66-
socket: (any BoltHeadSocket)? = nil
67-
) {
68-
self.init(
69-
thread: thread,
70-
length: length,
71-
unthreadedLength: unthreadedLength,
72-
unthreadedDiameter: unthreadedDiameter,
73-
headShape: headShape,
74-
socket: socket,
75-
point: ProfiledBoltPoint(chamferSize: leadinChamferSize)
76-
)
77-
}
78-
7951
/// Creates a bolt without threads, for cases where thread detail is unimportant.
8052
///
8153
/// - Parameters:
@@ -130,35 +102,50 @@ public struct Bolt: Shape3D {
130102
.withThread(thread)
131103
}
132104

133-
private func clearanceHoleDepth(recessedHead: Bool = false) -> Double {
134-
recessedHead ? (length + headShape.clearanceLength) : (length - headShape.consumedLength)
105+
/// The entry geometry for a bolt clearance hole.
106+
public enum ClearanceHoleEntry: Sendable {
107+
/// No special geometry at the entry.
108+
case plain
109+
/// An edge profile applied at the hole opening.
110+
case edgeProfile (EdgeProfile)
111+
/// A recess matching the bolt head shape.
112+
case recessedHead
135113
}
136114

137115
/// Creates a clearance hole sized for this bolt.
138116
///
139-
/// - Parameters:
140-
/// - depth: Hole depth. Defaults to the bolt length minus head consumption.
141-
/// - edgeProfile: Optional edge profile at the hole opening.
142-
/// - Returns: A clearance hole configured for this bolt.
143-
public func clearanceHole(depth: Double? = nil, edgeProfile: EdgeProfile? = nil) -> ClearanceHole {
144-
ClearanceHole(
145-
diameter: thread.majorDiameter,
146-
depth: depth ?? clearanceHoleDepth(),
147-
edgeProfile: edgeProfile
148-
)
149-
}
150-
151-
/// Creates a clearance hole sized for this bolt, optionally with a recess for the head.
117+
/// The default depth depends on the entry type: for ``ClearanceHoleEntry/recessedHead``,
118+
/// the hole extends deep enough to fully recess the head; otherwise, it extends
119+
/// the bolt length minus the head's consumed length.
152120
///
153121
/// - Parameters:
154-
/// - depth: Hole depth. Defaults to a depth that accommodates the full bolt length.
155-
/// - recessedHead: Whether to include a recess matching the bolt head shape.
122+
/// - depth: Hole depth. Defaults to a depth appropriate for the entry type.
123+
/// - entry: The geometry at the entry of the hole. Defaults to `.plain`.
156124
/// - Returns: A clearance hole configured for this bolt.
157-
public func clearanceHole(depth: Double? = nil, recessedHead: Bool) -> ClearanceHole {
125+
public func clearanceHole(depth: Double? = nil, entry: ClearanceHoleEntry = .plain) -> ClearanceHole {
126+
let recessedHead: Bool
127+
let clearanceEntry: ClearanceHole.Entry
128+
129+
switch entry {
130+
case .plain:
131+
recessedHead = false
132+
clearanceEntry = .plain
133+
case .edgeProfile(let profile):
134+
recessedHead = false
135+
clearanceEntry = .edgeProfile(profile)
136+
case .recessedHead:
137+
recessedHead = true
138+
clearanceEntry = .recess(headShape.recess)
139+
}
140+
141+
let defaultDepth = recessedHead
142+
? (length + headShape.clearanceLength)
143+
: (length - headShape.consumedLength)
144+
158145
return ClearanceHole(
159146
diameter: thread.majorDiameter,
160-
depth: depth ?? clearanceHoleDepth(recessedHead: true),
161-
boltHeadRecess: recessedHead ? headShape.recess : Empty()
147+
depth: depth ?? defaultDepth,
148+
entry: clearanceEntry
162149
)
163150
}
164151
}

Sources/Helical/Bolt/Parts/Heads/BoltHeadShape.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Foundation
21
import Cadova
32

43
/// A protocol defining the geometry and dimensions of a bolt head.

Sources/Helical/Bolt/Parts/Heads/CountersunkBoltHeadShape.swift

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Foundation
21
import Cadova
32

43
/// A conical countersunk bolt head that sits flush with or below the mounting surface.
@@ -10,8 +9,6 @@ public struct CountersunkBoltHeadShape: BoltHeadShape {
109
let boltDiameter: Double
1110
let lensHeight: Double
1211

13-
@Environment(\.tolerance) var tolerance
14-
1512
/// Creates a countersunk head with the specified countersink geometry.
1613
///
1714
/// - Parameters:
@@ -52,14 +49,15 @@ public struct CountersunkBoltHeadShape: BoltHeadShape {
5249
}
5350

5451
public var body: any Geometry3D {
52+
@Environment(\.tolerance) var tolerance
5553
let effectiveTopDiameter = countersink.topDiameter - tolerance
5654
let coneHeight = effectiveTopDiameter / 2 * tan(countersink.angle / 2)
5755

5856
Cylinder(bottomDiameter: effectiveTopDiameter, topDiameter: 0.001, height: coneHeight)
5957
.translated(z: lensHeight)
6058
.adding {
6159
if lensHeight > 0 {
62-
let diameter = lensHeight + pow(effectiveTopDiameter, 2) / (4 * lensHeight)
60+
let diameter = lensHeight + effectiveTopDiameter * effectiveTopDiameter / (4 * lensHeight)
6361
Sphere(diameter: diameter)
6462
.aligned(at: .minZ)
6563
.within(z: 0..<lensHeight)
@@ -73,23 +71,20 @@ public struct CountersunkBoltHeadShape: BoltHeadShape {
7371
}
7472

7573
public extension BoltHeadShape where Self == CountersunkBoltHeadShape {
74+
/// A countersunk bolt head with a specified cone angle.
75+
///
76+
/// - Parameters:
77+
/// - angle: The countersink cone angle. Defaults to 90°.
78+
/// - topDiameter: The diameter at the top of the cone.
79+
/// - boltDiameter: The bolt's major diameter.
7680
static func countersunk(
7781
angle: Angle = 90°,
7882
topDiameter: Double,
7983
boltDiameter: Double
8084
) -> CountersunkBoltHeadShape {
81-
countersunk(countersink: .init(angle: angle, topDiameter: topDiameter), boltDiameter: boltDiameter)
82-
}
83-
84-
static func countersunk(
85-
countersink: Countersink,
86-
boltDiameter: Double
87-
) -> CountersunkBoltHeadShape {
88-
.init(countersink: countersink, boltDiameter: boltDiameter)
89-
}
90-
91-
static func standardCountersunk(topDiameter: Double, boltDiameter: Double) -> CountersunkBoltHeadShape {
92-
.init(countersink: .init(angle: 90°, topDiameter: topDiameter), boltDiameter: boltDiameter)
85+
.init(
86+
countersink: Countersink(angle: angle, topDiameter: topDiameter),
87+
boltDiameter: boltDiameter
88+
)
9389
}
9490
}
95-

Sources/Helical/Bolt/Parts/Heads/CylindricalBoltHeadShape.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Foundation
21
import Cadova
32

43
/// A cylindrical bolt head, such as those used for socket head cap screws or button head screws.
@@ -13,11 +12,16 @@ public struct CylindricalBoltHeadShape: BoltHeadShape {
1312

1413
/// Creates a cylindrical head with optional edge profiles.
1514
///
15+
/// The "top" and "bottom" edges refer to the physical bolt orientation: the top
16+
/// is the visible face of the head, and the bottom is where it meets the shank.
17+
/// Internally, the head is modeled with Z=0 at the top and Z=height at the shank,
18+
/// so the edge profile parameters are swapped when passed to extrusion.
19+
///
1620
/// - Parameters:
1721
/// - diameter: The head diameter.
1822
/// - height: The head height.
19-
/// - topEdge: Optional edge profile for the top edge.
20-
/// - bottomEdge: Optional edge profile for the bottom edge.
23+
/// - topEdge: Optional edge profile for the top (visible) edge of the head.
24+
/// - bottomEdge: Optional edge profile for the bottom (shank-side) edge of the head.
2125
public init(diameter: Double, height: Double, topEdge: EdgeProfile? = nil, bottomEdge: EdgeProfile? = nil) {
2226
self.diameter = diameter
2327
self.height = height
@@ -43,6 +47,7 @@ public struct CylindricalBoltHeadShape: BoltHeadShape {
4347
public var body: any Geometry3D {
4448
@Environment(\.tolerance) var tolerance
4549

50+
// topEdge/bottomEdge are swapped because Z=0 is the head top, Z=height is the shank side
4651
Circle(diameter: diameter - tolerance)
4752
.extruded(height: height, topEdge: bottomEdge, bottomEdge: topEdge)
4853
.intersecting {
@@ -59,7 +64,14 @@ public struct CylindricalBoltHeadShape: BoltHeadShape {
5964
}
6065

6166
public extension BoltHeadShape where Self == CylindricalBoltHeadShape {
62-
static func cylindrical(
67+
/// A cylindrical bolt head with optional edge profiles.
68+
///
69+
/// - Parameters:
70+
/// - diameter: The head diameter.
71+
/// - height: The head height.
72+
/// - topEdge: Optional edge profile for the top (visible) edge of the head.
73+
/// - bottomEdge: Optional edge profile for the bottom (shank-side) edge of the head.
74+
static func cylinder(
6375
diameter: Double,
6476
height: Double,
6577
topEdge: EdgeProfile? = nil,

Sources/Helical/Bolt/Parts/Heads/PolygonalBoltHeadShape.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Foundation
21
import Cadova
32

43
/// A polygonal bolt head, such as a hex head or square head.
@@ -59,3 +58,26 @@ public struct PolygonalBoltHeadShape: BoltHeadShape {
5958
PolygonalHeadRecess(sideCount: sideCount, widthAcrossFlats: widthAcrossFlats, height: height)
6059
}
6160
}
61+
62+
public extension BoltHeadShape where Self == PolygonalBoltHeadShape {
63+
/// A polygonal bolt head with chamfered corners.
64+
///
65+
/// - Parameters:
66+
/// - sideCount: Number of sides (6 for hex, 4 for square, etc.).
67+
/// - widthAcrossFlats: Distance between opposite flat faces.
68+
/// - height: The head height.
69+
/// - chamferAngle: Angle of the corner chamfer.
70+
static func polygon(sideCount: Int, widthAcrossFlats: Double, height: Double, chamferAngle: Angle = 0°) -> Self {
71+
.init(sideCount: sideCount, widthAcrossFlats: widthAcrossFlats, height: height, chamferAngle: chamferAngle)
72+
}
73+
74+
/// A hex bolt head with chamfered corners.
75+
///
76+
/// - Parameters:
77+
/// - widthAcrossFlats: Distance between opposite flat faces.
78+
/// - height: The head height.
79+
/// - chamferAngle: Angle of the corner chamfer.
80+
static func hex(widthAcrossFlats: Double, height: Double, chamferAngle: Angle = 0°) -> Self {
81+
.init(sideCount: 6, widthAcrossFlats: widthAcrossFlats, height: height, chamferAngle: chamferAngle)
82+
}
83+
}

0 commit comments

Comments
 (0)