One slope line to the ground
64-bitThe point where a cut or fill slope leaving the pad edge meets the surface.
f(d) = z + g·d − ground(p₀ + u·d); g = rate ÷ cut or −rate ÷ fill; find d with f(d) = 0
Marches in fixed steps until f changes sign, then bisects until the bracket is 1e-7 or 60 halvings, and returns the middle of the bracket, so the distance is within 5e-8 project units of the crossing that was bracketed. The tolerance is absolute. A ray that leaves the surface or reaches the far limit is returned as 'off'.
/**
* One slope line: from p0 (at design elevation z) outward along (ux, uy), up at `rate`/cut where the
* ground is above z, down at `rate`/fill where it is below, to where it meets the surface.
* → { x0, y0, x, y, z, d, kind: 'cut'|'fill'|'flat'|'off' } ('off': left the surface first)
*/
export function castSlope(surface, p0, ux, uy, z, cut, fill, { rate = 1, step = 0.25, far = 1e4 } = {}) {
const z0 = surface.elevationAt(p0.x, p0.y, 1e-6);
if (z0 === null) return { x0: p0.x, y0: p0.y, x: p0.x, y: p0.y, z, d: 0, kind: 'off' };
if (Math.abs(z0 - z) < 1e-9) return { x0: p0.x, y0: p0.y, x: p0.x, y: p0.y, z, d: 0, kind: 'flat' };
const kind = z0 > z ? 'cut' : 'fill';
const g = kind === 'cut' ? rate / cut : -rate / fill; // design rise per unit outward
const f = (d) => { const e = surface.elevationAt(p0.x + ux * d, p0.y + uy * d, 1e-6); return e === null ? null : z + g * d - e; };
// f(0) has the sign of (design − ground); march until it changes, then bisect
const s0 = Math.sign(f(0));
let a = 0, b = step, fb = f(b);
while (fb !== null && Math.sign(fb) === s0 && b < far) { a = b; b += step; fb = f(b); }
if (fb === null || b >= far) return { x0: p0.x, y0: p0.y, x: p0.x + ux * a, y: p0.y + uy * a, z: z + g * a, d: a, kind: 'off' };
for (let it = 0; it < 60 && b - a > 1e-7; it++) { const m = (a + b) / 2, fm = f(m); if (fm !== null && Math.sign(fm) === s0) a = m; else b = m; }
const d = (a + b) / 2;
return { x0: p0.x, y0: p0.y, x: p0.x + ux * d, y: p0.y + uy * d, z: z + g * d, d, kind };
}