99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import type { Vec2 } from './geometry';
|
||
|
||
/**
|
||
* Coping axis as a polyline IP1 → (IP4) → IP3 (spec §4.1). Each stretch is straight and has its own frame.
|
||
* Coordinates: k = stretch index, s = arc length from IP1 (the same s for both stretches), n = offset to the right of u.
|
||
* The lateral offset uses the same convention as the straight model: nrm = (u.n, −u.e).
|
||
*/
|
||
export interface AxisSegment {
|
||
start: Vec2;
|
||
u: Vec2;
|
||
nrm: Vec2;
|
||
/** Arc length at the start of this stretch. */
|
||
s0: number;
|
||
}
|
||
|
||
export interface Axis {
|
||
segments: AxisSegment[];
|
||
/** Total arc length L_IP. */
|
||
length: number;
|
||
/** Bend point IP4, or null for a straight axis. */
|
||
kink: Vec2 | null;
|
||
/** Mitre normal u1 + u2 (unit) at the kink; null for a straight axis. A point is on stretch 0 when (p − kink)·mitre ≤ 0. */
|
||
mitre: Vec2 | null;
|
||
}
|
||
|
||
export interface AxisPoint {
|
||
k: number;
|
||
s: number;
|
||
n: number;
|
||
}
|
||
|
||
const dist = (a: Vec2, b: Vec2) => Math.hypot(b.e - a.e, b.n - a.n);
|
||
const sub = (a: Vec2, b: Vec2): Vec2 => ({ e: a.e - b.e, n: a.n - b.n });
|
||
const dot = (a: Vec2, b: Vec2) => a.e * b.e + a.n * b.n;
|
||
const unit = (v: Vec2): Vec2 => {
|
||
const len = Math.hypot(v.e, v.n);
|
||
if (len === 0) throw new Error('축 길이가 0입니다.');
|
||
return { e: v.e / len, n: v.n / len };
|
||
};
|
||
|
||
function frame(start: Vec2, end: Vec2): { u: Vec2; nrm: Vec2 } {
|
||
const u = unit(sub(end, start));
|
||
return { u, nrm: { e: u.n, n: -u.e } };
|
||
}
|
||
|
||
export function buildAxis(ip1: Vec2, ip3: Vec2, kink: Vec2 | null): Axis {
|
||
if (!kink) {
|
||
const f = frame(ip1, ip3);
|
||
return { segments: [{ start: ip1, ...f, s0: 0 }], length: dist(ip1, ip3), kink: null, mitre: null };
|
||
}
|
||
const a = dist(ip1, kink);
|
||
const f1 = frame(ip1, kink);
|
||
const f2 = frame(kink, ip3);
|
||
return {
|
||
segments: [
|
||
{ start: ip1, ...f1, s0: 0 },
|
||
{ start: kink, ...f2, s0: a },
|
||
],
|
||
length: a + dist(kink, ip3),
|
||
kink,
|
||
mitre: unit({ e: f1.u.e + f2.u.e, n: f1.u.n + f2.u.n }),
|
||
};
|
||
}
|
||
|
||
/** Stretch and local coordinates of a world point. Points on the stretch-0 side of the mitre plane belong to stretch 0. */
|
||
export function toAxis(axis: Axis, p: Vec2): AxisPoint {
|
||
const k = axis.mitre && axis.kink && dot(sub(p, axis.kink), axis.mitre) > 0 ? 1 : 0;
|
||
const seg = axis.segments[k];
|
||
const d = sub(p, seg.start);
|
||
return { k, s: seg.s0 + dot(d, seg.u), n: dot(d, seg.nrm) };
|
||
}
|
||
|
||
/** World point of local coordinates (inverse of toAxis for the same k). */
|
||
export function fromAxis(axis: Axis, q: AxisPoint): Vec2 {
|
||
const seg = axis.segments[q.k];
|
||
const ds = q.s - seg.s0;
|
||
return {
|
||
e: seg.start.e + ds * seg.u.e + q.n * seg.nrm.e,
|
||
n: seg.start.n + ds * seg.u.n + q.n * seg.nrm.n,
|
||
};
|
||
}
|
||
|
||
/** Arc-length span of the coping of length L, centred on the axis (spec §4.2: centre at L_IP / 2). */
|
||
export function copingSpan(axis: Axis, L: number): { sStart: number; sEnd: number } {
|
||
return { sStart: axis.length / 2 - L / 2, sEnd: axis.length / 2 + L / 2 };
|
||
}
|
||
|
||
/** Arc length of the kink, or null for a straight axis. */
|
||
export function kinkArc(axis: Axis): number | null {
|
||
return axis.kink ? axis.segments[1].s0 : null;
|
||
}
|
||
|
||
/** Positions (arc length) closer than minDistance to the kink. Used to refuse columns at the bend. */
|
||
export function nearKink(axis: Axis, sPositions: number[], minDistance: number): number[] {
|
||
const a = kinkArc(axis);
|
||
if (a === null) return [];
|
||
return sPositions.filter((s) => Math.abs(s - a) < minDistance);
|
||
}
|