///| Geometry in consistent length units. Positive offset is along the x axis
///|
/// at zero cam angle. Cam coordinates use a clockwise angular parameter.
pub(all) struct Geometry {
base_radius : Double
roller_radius : Double
offset : Double
}
///|
/// Pitch center, cam contact, outward normal and differential geometry.
pub(all) struct CamPoint {
theta : Double
motion : Motion
pitch_x : Double
pitch_y : Double
x : Double
y : Double
normal_x : Double
normal_y : Double
pressure : Double
curvature : Double
offset_factor : Double
}
///|
pub fn validate_geometry(g : Geometry) -> Result[Unit, String] {
if !finite(g.base_radius) || !finite(g.roller_radius) || !finite(g.offset) {
return Err("geometry must be finite")
}
if g.base_radius < Base_radius_min ||
g.base_radius > Geometry_length_max ||
g.roller_radius <= 0.0 ||
g.roller_radius > Geometry_length_max {
return Err(
"base radius must be in [\{Base_radius_min}, \{Geometry_length_max}]; roller radius in (0, \{Geometry_length_max}]",
)
}
if g.offset.abs() >= g.base_radius + g.roller_radius {
return Err("offset magnitude must be smaller than prime-circle radius")
}
Ok(())
}
///|
/// Synthesize the roller-envelope contact point, not a radial subtraction.
pub fn contact(
p : Program,
g : Geometry,
theta : Double,
) -> Result[CamPoint, String] {
match validate_geometry(g) {
Err(e) => return Err(e)
Ok(_) => ()
}
let m = match evaluate(p, theta) {
Err(e) => return Err(e)
Ok(m) => m
}
contact_prepared(g, zero_height(g), theta, m)
}
///|
/// Cached once per geometry, shared by sampling and metrics-only search.
fn zero_height(g : Geometry) -> Double {
let r = g.base_radius + g.roller_radius
((r - g.offset) * (r + g.offset)).sqrt()
}
///|
/// No trigonometry or CamPoint allocation is required for scalar checks.
priv struct LocalGeometry {
q : Double
speed : Double
pressure : Double
curvature : Double
factor : Double
}
///|
fn local_geometry(
g : Geometry,
q0 : Double,
m : Motion,
) -> Result[LocalGeometry, String] {
let q = q0 + m.s
let d = m.v - g.offset
let speed = @math.hypot(q, d)
let k = (q * (q - m.a) + d * (2.0 * m.v - g.offset)) / (speed * speed * speed)
let factor = 1.0 - g.roller_radius * k
if !finite(q) || !finite(k) || !finite(factor) || speed <= 0.0 {
return Err("non-finite or singular contact geometry")
}
Ok({ q, speed, pressure: @math.atan2(d, q), curvature: k, factor, })
}
///|
fn contact_prepared(
g : Geometry,
q0 : Double,
theta : Double,
m : Motion,
) -> Result[CamPoint, String] {
let geom = match local_geometry(g, q0, m) {
Ok(v) => v
Err(e) => return Err(e)
}
contact_local(g, theta, m, geom)
}
///|
/// Construct full coordinates only for retained output points.
fn contact_local(
g : Geometry,
theta : Double,
m : Motion,
geom : LocalGeometry,
) -> Result[CamPoint, String] {
let q = geom.q
let sn = @math.sin(theta)
let cs = @math.cos(theta)
let px = q * sn + g.offset * cs
let py = q * cs - g.offset * sn
let dx = q * cs + (m.v - g.offset) * sn
let dy = (m.v - g.offset) * cs - q * sn
let speed = geom.speed
let nx = -dy / speed
let ny = dx / speed
let x = px - g.roller_radius * nx
let y = py - g.roller_radius * ny
if !finite(x) || !finite(y) {
return Err("non-finite contact coordinates")
}
Ok({
theta,
motion: m,
pitch_x: px,
pitch_y: py,
x,
y,
normal_x: nx,
normal_y: ny,
pressure: geom.pressure,
curvature: geom.curvature,
offset_factor: geom.factor,
})
}