///|
// Conical hexagon socket: a 45-degree countersink at the mouth and a 70-degree
// drill point at the floor, each trimmed by the six socket flats. The six rim
// patches and the drill patch reference one `CONICAL_SURFACE` each, so the
// analytic surface inventory is exactly the detailed ISO 4762 reference
// (plane 9, cone 5, cylinder 2) while every hex wall keeps its exact hyperbola
// boundary. Pcurves are attached on both adjacent faces (flat/cone or
// plane/cone) so importers never have to reconstruct a trimmed cone.
///|
priv struct ConicalSocketGeometry {
top_z : Double
top_radius : Double
d : Double
r_corner : Double
hex_area : Double
rim_apex : Double
rim_tan : Double
floor_apex : Double
floor_tan : Double
rotation_deg : Double
}
///|
fn conical_socket_geometry(s : SocketedSolid) -> ConicalSocketGeometry? {
match s.body.validate() {
Err(_) => return None
Ok(_) => ()
}
let segs = s.body.segments
let n = segs.length()
if n < 2 {
return None
}
let (top_radius, top_z) = match segs[n - 1] {
RevLine(a, b) =>
if b.0.abs() > CAD_LENGTH_EPS ||
(a.1 - b.1).abs() > CAD_LENGTH_EPS ||
a.0 <= 0.0 {
return None
} else {
(a.0, b.1)
}
RevArc(_, _, _) => return None
}
let af = s.across_flats_mm
let rim_tan = @math.tan(s.rim_angle_deg * @math.PI / 180.0)
let floor_tan = @math.tan(s.floor_angle_deg * @math.PI / 180.0)
if af <= 0.0 || rim_tan <= 0.0 || floor_tan <= 0.0 {
return None
}
let d = af / 2.0
let r_corner = af / Double::sqrt(3.0)
let rim_apex = top_z - r_corner / rim_tan
if s.socket_floor_z_mm >= rim_apex - CAD_LENGTH_EPS ||
s.socket_floor_z_mm >= top_z - CAD_LENGTH_EPS {
return None
}
if top_radius <= r_corner + CAD_LENGTH_EPS {
return None
}
Some({
top_z,
top_radius,
d,
r_corner,
hex_area: Double::sqrt(3.0) / 2.0 * af * af,
rim_apex,
rim_tan,
floor_apex: s.socket_floor_z_mm,
floor_tan,
rotation_deg: s.rotation_deg,
})
}
///|
/// conical_circular_segment returns the area of one circular segment cut off
/// by a hexagon flat at distance `d` from the axis, for `d <= r <= r_corner`.
fn conical_circular_segment(
r : Double,
d : Double,
r_corner : Double,
) -> Double {
if r <= d {
return 0.0
}
let rc = if r > r_corner { r_corner } else { r }
rc * rc * @math.acos(d / rc) - d * Double::sqrt(rc * rc - d * d)
}
///|
/// conical_socket_area_at returns the exact void cross-section area at `z`.
/// The void is the hexagon intersected with the drill cone, united with the
/// rim countersink disk.
fn conical_socket_area_at(g : ConicalSocketGeometry, z : Double) -> Double {
if z < g.floor_apex - CAD_LENGTH_EPS || z > g.top_z + CAD_LENGTH_EPS {
return 0.0
}
let rr = if z > g.rim_apex { (z - g.rim_apex) * g.rim_tan } else { 0.0 }
let rf = if z > g.floor_apex { (z - g.floor_apex) * g.floor_tan } else { 0.0 }
if rr >= rf && rr > 0.0 {
return @math.PI * rr * rr
}
// area(hexagon intersect drill disk)
let hex_part = if rf <= g.d {
@math.PI * rf * rf
} else if rf >= g.r_corner {
g.hex_area
} else {
@math.PI * rf * rf - 6.0 * conical_circular_segment(rf, g.d, g.r_corner)
}
// area(rim disk outside the hexagon)
let rim_part = if rr <= g.d {
0.0
} else if rr >= g.r_corner {
@math.PI * rr * rr - g.hex_area
} else {
6.0 * conical_circular_segment(rr, g.d, g.r_corner)
}
hex_part + rim_part
}
///|
/// conical_socket_breakpoints returns every axial kink of the area function so
/// Simpson's rule samples smooth spans only.
fn conical_socket_breakpoints(g : ConicalSocketGeometry) -> Array[Double] {
let zs : Array[Double] = [g.floor_apex, g.top_z]
zs.push(g.floor_apex + g.d / g.floor_tan)
zs.push(g.floor_apex + g.r_corner / g.floor_tan)
zs.push(g.rim_apex + g.d / g.rim_tan)
zs.push(g.rim_apex + g.r_corner / g.rim_tan)
let clipped : Array[Double] = []
for z in zs {
if z > g.floor_apex - CAD_LENGTH_EPS && z < g.top_z + CAD_LENGTH_EPS {
clipped.push(z)
}
}
let sorted = an_sort_unique(clipped, 1.0e-9)
sorted
}
///|
/// conical_socket_volume_centroid integrates the void exactly by composite
/// Simpson over the kink-free spans.
fn conical_socket_volume_centroid(
g : ConicalSocketGeometry,
) -> (Double, Double) {
let zs = conical_socket_breakpoints(g)
let mut volume = 0.0
let mut moment = 0.0
let samples = 64
for i in 0..<(zs.length() - 1) {
let z0 = zs[i]
let z1 = zs[i + 1]
let h = (z1 - z0) / Double::from_int(samples)
if h <= CAD_DEGENERATE_EPS {
continue
}
let mut span = 0.0
let mut span_m = 0.0
for j in 0..<=samples {
let z = z0 + h * Double::from_int(j)
let a = conical_socket_area_at(g, z)
let w = if j == 0 || j == samples {
1.0
} else if j % 2 == 1 {
4.0
} else {
2.0
}
span = span + w * a
span_m = span_m + w * a * z
}
span = span * h / 3.0
span_m = span_m * h / 3.0
volume = volume + span
moment = moment + span_m
}
if volume <= CAD_DEGENERATE_EPS {
(0.0, 0.0)
} else {
(volume, moment / volume)
}
}
///|
/// conical_socket_void_perimeter returns the void boundary length at `z` in
/// polar form: rho(theta) = max(min(flat_distance, drill_radius), rim_radius).
fn conical_socket_void_perimeter(
g : ConicalSocketGeometry,
z : Double,
) -> Double {
if z < g.floor_apex - CAD_LENGTH_EPS || z > g.top_z + CAD_LENGTH_EPS {
return 0.0
}
let rr = if z > g.rim_apex { (z - g.rim_apex) * g.rim_tan } else { 0.0 }
let rf = if z > g.floor_apex { (z - g.floor_apex) * g.floor_tan } else { 0.0 }
let base = g.rotation_deg * @math.PI / 180.0
let period = @math.PI / 3.0
let n = 1440
let step = 2.0 * @math.PI / Double::from_int(n)
let rho_at = fn(theta : Double) -> Double {
let offset = theta - base - period / 2.0
let sector = offset - period * Double::floor(offset / period)
let delta = sector - period / 2.0
let wall = g.d / @math.cos(delta)
let inner = if wall < rf { wall } else { rf }
if inner > rr {
inner
} else {
rr
}
}
let mut length = 0.0
let cos_step = @math.cos(step)
for i in 0.. RevCircle {
rev_get_circle(ctx, xform, g.top_radius, g.top_z, circles)
}
///|
/// compile_conical_socketed_solid emits a revolved body whose flat top cap is
/// replaced by a conical countersink, six hyperbola-trimmed flats, and a
/// conical drill floor. Every shared edge carries a pcurve on both adjacent
/// surfaces.
pub fn compile_conical_socketed_solid(
ctx : StepContext,
node : SceneNode,
s : SocketedSolid,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
let xform = node_transform(node, parent)
let g = match conical_socket_geometry(s) {
Some(v) => v
None => return []
}
let up = xform.apply_dir((0.0, 0.0, 1.0))
let right = xform.apply_dir((1.0, 0.0, 0.0))
let dir_up = ctx.direction(up.0, up.1, up.2)
let dir_right = ctx.direction(right.0, right.1, right.2)
let context_2d = ctx.context_2d()
let circles : Map[String, RevCircle] = Map([])
let faces : Array[@step.Ref] = []
// Every revolved band except the flat top cap.
let segs = s.body.segments
let n = segs.length()
for i in 0..<(n - 1) {
let band = socketed_band(segs[i])
match rev_compile_band(ctx, xform, band, up, right, circles, reverse~) {
Some(f) => faces.push(f)
None => ()
}
}
let top_circle = conical_socket_top_circle(ctx, xform, g, circles)
// Surfaces. One conical entity per cone; every patch references it.
let top_plane = hhp_plane(ctx, xform.apply((0.0, 0.0, g.top_z)), up, right)
let rim_cone = hhp_cone(
ctx,
xform.apply((0.0, 0.0, g.rim_apex)),
up,
right,
0.0,
s.rim_angle_deg,
)
let drill_cone = hhp_cone(
ctx,
xform.apply((0.0, 0.0, g.floor_apex)),
up,
right,
0.0,
s.floor_angle_deg,
)
let flats : Array[HHPlaneRef] = []
for i in 0..<6 {
let psi = (g.rotation_deg + 30.0 + 60.0 * Double::from_int(i)) *
@math.PI /
180.0
// The plane axis points into the pocket (the face's outward normal): the
// material is outside the hexagon, so the pocket wall faces the void.
let normal = v_norm(
xform.apply_dir((-@math.cos(psi), -@math.sin(psi), 0.0)),
)
let origin = xform.apply((g.d * @math.cos(psi), g.d * @math.sin(psi), 0.0))
let tangent = v_norm(
xform.apply_dir((-@math.sin(psi), @math.cos(psi), 0.0)),
)
flats.push(hhp_plane(ctx, origin, normal, tangent))
}
// Vertices.
let corner_angle = fn(i : Int32) -> Double {
(g.rotation_deg + 60.0 * Double::from_int(i)) * @math.PI / 180.0
}
let z_b = g.floor_apex + g.r_corner / g.floor_tan
let top_pts : Array[Vec3] = []
let bot_pts : Array[Vec3] = []
for i in 0..<6 {
let a = corner_angle(i)
top_pts.push(
xform.apply(
(g.r_corner * @math.cos(a), g.r_corner * @math.sin(a), g.top_z),
),
)
bot_pts.push(
xform.apply((g.r_corner * @math.cos(a), g.r_corner * @math.sin(a), z_b)),
)
}
let top_cp = top_pts.map(fn(p) { hhwf_cp(ctx, p) })
let bot_cp = bot_pts.map(fn(p) { hhwf_cp(ctx, p) })
let top_vp = top_cp.map(fn(c) { ctx.vertex_point(c) })
let bot_vp = bot_cp.map(fn(c) { ctx.vertex_point(c) })
// Opening-circle arcs, shared by the annulus and the rim patches.
let arcs : Array[@step.Ref] = []
for i in 0..<6 {
let a0 = corner_angle(i)
let a1 = a0 + @math.PI / 3.0
let center_cp = hhwf_cp(ctx, xform.apply((0.0, 0.0, g.top_z)))
let a2p = ctx.axis2_placement_3d(center_cp, dir_up, dir_right)
let circle = ctx.add(
@repository.any_entity_circle(
@geometry.Circle::new("", a2p, @step.fmt_num(g.r_corner)),
),
)
let samples : Array[Vec3] = []
for k in 0..<=12 {
let t = Double::from_int(k) / 12.0
let a = a0 + (a1 - a0) * t
samples.push(
xform.apply(
(g.r_corner * @math.cos(a), g.r_corner * @math.sin(a), g.top_z),
),
)
}
arcs.push(
hhp_surface_edge(
ctx,
circle,
samples,
top_plane,
None,
Some(rim_cone),
context_2d,
top_vp[i],
top_vp[(i + 1) % 6],
),
)
}
// Top hyperbolas (rim cone trimmed by each flat).
let hyper_top : Array[@step.Ref] = []
for i in 0..<6 {
let psi = (g.rotation_deg + 30.0 + 60.0 * Double::from_int(i)) *
@math.PI /
180.0
let center = xform.apply(
(g.d * @math.cos(psi), g.d * @math.sin(psi), g.rim_apex),
)
let axis = v_norm(xform.apply_dir((@math.cos(psi), @math.sin(psi), 0.0)))
let a2p = ctx.axis2_placement_3d(
hhwf_cp(ctx, center),
ctx.direction(axis.0, axis.1, axis.2),
ctx.direction(up.0, up.1, up.2),
)
let a = g.d / g.rim_tan
let hyperbola = ctx.add(
@repository.any_entity_hyperbola(
@geometry.Hyperbola::new("", a2p, a, g.d),
),
)
let samples : Array[Vec3] = []
let a0 = corner_angle(i)
for k in 0..<=48 {
let t = Double::from_int(k) / 48.0
let theta = a0 + @math.PI / 3.0 * t
let r = g.d / @math.cos(theta - psi)
let z = g.rim_apex + r / g.rim_tan
samples.push(xform.apply((r * @math.cos(theta), r * @math.sin(theta), z)))
}
hyper_top.push(
hhp_surface_edge(
ctx,
hyperbola,
samples,
flats[i],
None,
Some(rim_cone),
context_2d,
top_vp[i],
top_vp[(i + 1) % 6],
edge_forward=false,
),
)
}
// Bottom hyperbolas (drill cone trimmed by each flat).
let hyper_bot : Array[@step.Ref] = []
for i in 0..<6 {
let psi = (g.rotation_deg + 30.0 + 60.0 * Double::from_int(i)) *
@math.PI /
180.0
let center = xform.apply(
(g.d * @math.cos(psi), g.d * @math.sin(psi), g.floor_apex),
)
let axis = v_norm(xform.apply_dir((@math.cos(psi), @math.sin(psi), 0.0)))
let a2p = ctx.axis2_placement_3d(
hhwf_cp(ctx, center),
ctx.direction(axis.0, axis.1, axis.2),
ctx.direction(up.0, up.1, up.2),
)
let a = g.d / g.floor_tan
let hyperbola = ctx.add(
@repository.any_entity_hyperbola(
@geometry.Hyperbola::new("", a2p, a, g.d),
),
)
let samples : Array[Vec3] = []
let a0 = corner_angle(i)
for k in 0..<=48 {
let t = Double::from_int(k) / 48.0
let theta = a0 + @math.PI / 3.0 * t
let r = g.d / @math.cos(theta - psi)
let z = g.floor_apex + r / g.floor_tan
samples.push(xform.apply((r * @math.cos(theta), r * @math.sin(theta), z)))
}
hyper_bot.push(
hhp_surface_edge(
ctx,
hyperbola,
samples,
flats[i],
None,
Some(drill_cone),
context_2d,
bot_vp[i],
bot_vp[(i + 1) % 6],
edge_forward=false,
),
)
}
// Vertical corner edges (shared by the two adjacent flats).
let verticals : Array[@step.Ref] = []
for i in 0..<6 {
let dir = v_norm(v_sub(bot_pts[i], top_pts[i]))
let curve = ctx.add(
@repository.any_entity_line(
@geometry.Line::new(
"",
top_cp[i],
ctx.direction(dir.0, dir.1, dir.2) |> d => { ctx.vector(d, 1.0) },
),
),
)
verticals.push(
hhp_surface_edge(
ctx,
curve,
[top_pts[i], bot_pts[i]],
flats[(i + 5) % 6],
Some(flats[i]),
None,
context_2d,
top_vp[i],
bot_vp[i],
),
)
}
// Drill seam: apex to the first bottom corner, on the drill cone only.
let apex_pt = xform.apply((0.0, 0.0, g.floor_apex))
let apex_cp = hhwf_cp(ctx, apex_pt)
let apex_vp = ctx.vertex_point(apex_cp)
let seam_dir = v_norm(v_sub(bot_pts[0], apex_pt))
let seam_curve = ctx.add(
@repository.any_entity_line(
@geometry.Line::new(
"",
apex_cp,
ctx.direction(seam_dir.0, seam_dir.1, seam_dir.2)
|> d => { ctx.vector(d, 1.0) },
),
),
)
let seam_pc = hhp_pcurve_polyline(
ctx,
drill_cone.surface,
[hhp_cone_uv(apex_pt, drill_cone), hhp_cone_uv(bot_pts[0], drill_cone)],
context_2d,
None,
)
let seam_edge = ctx.edge_curve(
apex_vp,
bot_vp[0],
ctx.surface_curve(seam_curve, [seam_pc]),
true,
)
// Flat faces.
for i in 0..<6 {
let face_loop = ctx.edge_loop([
ctx.oriented_edge(hyper_top[i], true),
ctx.oriented_edge(verticals[(i + 1) % 6], true),
ctx.oriented_edge(hyper_bot[i], false),
ctx.oriented_edge(verticals[i], false),
])
faces.push(
ctx.advanced_face(
[ctx.face_outer_bound(face_loop, true)],
flats[i].surface,
!reverse,
),
)
}
// Rim cone patches (one per flat), all sharing the rim cone surface.
for i in 0..<6 {
let psi = (g.rotation_deg + 30.0 + 60.0 * Double::from_int(i)) *
@math.PI /
180.0
let natural = v_norm(
xform.apply_dir(
(-@math.cos(psi) / g.rim_tan, -@math.sin(psi) / g.rim_tan, 1.0),
),
)
let a0 = corner_angle(i)
let mid_theta = a0 + @math.PI / 6.0
let mid_r = g.d / @math.cos(mid_theta - psi)
let mid = xform.apply(
(
mid_r * @math.cos(mid_theta),
mid_r * @math.sin(mid_theta),
g.rim_apex + mid_r / g.rim_tan,
),
)
let face_loop = ctx.edge_loop([
ctx.oriented_edge(arcs[i], true),
ctx.oriented_edge(hyper_top[i], false),
])
let _ = mid
let _ = natural
let _ = top_pts
// Pocket cone faces are the mirror sense of the exterior hex-head chamfer:
// the detailed references write them `.F.`.
faces.push(
ctx.advanced_face(
[ctx.face_outer_bound(face_loop, true)],
rim_cone.surface,
reverse,
),
)
}
// Drill cone face: seam out, six hyperbolas, seam back.
let drill_loop_edges : Array[@step.Ref] = [ctx.oriented_edge(seam_edge, true)]
for i in 0..<6 {
drill_loop_edges.push(ctx.oriented_edge(hyper_bot[i], true))
}
drill_loop_edges.push(ctx.oriented_edge(seam_edge, false))
let drill_loop = ctx.edge_loop(drill_loop_edges)
faces.push(
ctx.advanced_face(
[ctx.face_outer_bound(drill_loop, true)],
drill_cone.surface,
reverse,
),
)
// Annular top face: body top circle outside, opening arcs inside.
let outer_loop = ctx.edge_loop([ctx.oriented_edge(top_circle.edge, true)])
let inner_edges : Array[@step.Ref] = []
for k in 0..<6 {
inner_edges.push(ctx.oriented_edge(arcs[5 - k], false))
}
let inner_loop = ctx.edge_loop(inner_edges)
// Reuse the top plane entity the arc pcurves reference, so every pcurve names
// its own face's surface.
faces.push(
ctx.advanced_face(
[ctx.face_outer_bound(outer_loop, true), ctx.face_bound(inner_loop, true)],
top_plane.surface,
!reverse,
),
)
faces
}