Files
Pier_v1/src/core/axis.ts
T
ypshimandClaude Haiku 5.5 46709be532 feat(core): polyline coping axis with per-stretch frames (bent coping stage 1)
- buildAxis: IP1 → IP4 → IP3 as stretches with their own frames; arc length s
  from IP1; mitre normal u1 + u2 at the kink.
- toAxis / fromAxis: world ↔ (k, s, n) round trip; stretch chosen by the mitre plane.
- copingSpan (centred on L_IP/2) and nearKink (columns within the Level half-width).
- Straight P1 regression against spec §12 bearing values; bent round trip and continuity.
- Not wired into buildPier yet; the model still refuses bent coping (미실시).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-22 09:18:31 +09:00

99 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}