Files
Pier_v1/src/core/abut/geometry.ts
T
ypshimandClaude Haiku 5.5 4b8b80ae97 feat(abut): 접속슬래브 카드 — 매립형 / 노출형 형식, 경사, 치수 입력, 삽도 3개, 모따기 3D 반영
- 노출형 (삽도 기준, QA-8 가정): 상면 = 흉벽 상단, 끝은 브라켓 위(끝 두께 = 브라켓 높이), 헌치 길이 기본 2000
- 형식을 고르면 경사가 기본값으로 바뀜 (매립형 하향 4 %, 노출형 종단경사), 경사 타일로 변경 가능
- 길이 · 두께 · 흉벽과 간격 · 날개벽과 간격 입력이 3D, 폭, 날개벽 평면에 반영
- 슬래브 모따기 = 날개벽 헌치 (3D와 평면), 날개벽 없는 측은 skew 교좌면 끝선
- 3D 슬래브를 단면 기준 입체로 다시 만듦, 프로젝트 파일 저장 · 검증

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-04 10:11:39 +09:00

212 lines
10 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.
/**
* 교대 형상 · AA1 (2026-11-03, spec_abut §1·§4.1–4.2): 교대축, 전면 쪽, 교좌면 위의 받침, 교좌면 EL과 블럭 높이를 계산한다.
* 순수 함수. 좌표는 (E, N) m 단위, EL은 m 단위, 길이는 별도 표기가 없으면 mm 단위다.
*
* - 축: IP1 → IP3 선(교대중심선), 길이 L. u = IP1 → IP3 단위벡터, nLeft = u를 반시계 90° 돌린 벡터.
* - 받침 s: IP1에서 u 방향으로 잰 거리(m). 축에서 벗어난 거리(lateral): nLeft 방향 거리(m).
* - z = 패드 상면(EL.X), 교각과 같다(2026-11-03 결정 1). 교좌면에서 패드 상면까지 블럭 높이는 125 mm
* (콘크리트 75 + 패드 50). 교좌면 EL은 콘크리트 하면이다.
* - 교좌면: 교대 전체에 교좌면 EL 하나를 입력받는다(첫 받침 ①). 블럭 높이 h = min(z) − 교좌면으로 모든 받침이 같고,
* 받침 i의 교좌면 = z_i − h. 받침이 자기 교좌면 EL(seatEl)을 직접 주면 그 값을 쓰고, 블럭 높이는 z_i − seatEl이 된다
* (2026-11-03 결정 3).
* - 전면: 받침이 있는 쪽. A1은 전면이 1 → n 방향의 왼쪽, 흉벽은 오른쪽에 있다. A2는 반대다(spec_abut §1).
* 맞지 않으면 AW03 경고 대상이다.
*/
export type AbutType = 'A1' | 'A2';
export interface AbutPoint {
e: number;
n: number;
}
export interface AbutBearing {
no: number;
/** 위치 (E, N), m. */
pos: AbutPoint;
/** 패드 상면 EL (z), m. */
z: number;
/** 이 받침의 교좌면 EL (m). 없으면 z에서 교대 블럭 높이를 뺀 값을 쓴다. */
seatEl?: number;
/** 받침 방위각 (deg, model-xy: 북에서 시계방향, 교각과 같은 변환 swapXYAzimuth). 없으면 블럭은 교대축에 맞춘다. */
azimuthDeg?: number;
}
/** 날개벽 형식, 측별(spec_abut §4.7): 일반형 / 단축형 / 없음. */
export type WingForm = 'general' | 'short' | 'none';
export interface AbutInput {
name: string;
type: AbutType;
/** 첫 받침(구간 ①)의 교좌면 EL (m). 블럭 높이 h = min(z) − seatEl. */
seatEl: number;
ip1: AbutPoint;
ip2: AbutPoint;
ip3: AbutPoint;
bearings: AbutBearing[];
/** skew (deg): 받침축을 시계방향으로 돌린 선이 날개벽 선이다(spec_abut §1). 없으면 날개벽 검사를 하지 않는다. */
skewDeg?: number;
/** 종단경사 i (소수, 교량진행방향 기준, (−) = 낮아짐). 날개벽 경사는 A1이 −i, A2가 +i다. */
slopeI?: number;
/** AW01용 거더 방향 CAD 각 (deg). 없으면 AW01을 검사하지 않는다. */
girderAzimuthDeg?: number;
/** 측별 날개벽 형식: 'left' = 받침 ① 쪽 끝, 'right' = 받침 n 쪽 끝(spec §4.7). 기본은 양측 일반형. */
wings?: { left: WingForm; right: WingForm };
/** 측별 날개벽 두께 (mm). 없으면 450. 접속슬래브 폭에 쓴다. */
wingThicknessMm?: { left?: number; right?: number };
/** 접속슬래브와 날개벽 안쪽면 사이 간격 (mm, ② 접속슬래브 카드). 없으면 20. */
slabWingGapMm?: number;
/** 기초 하단 EL (m), 말뚝길이 (mm), 기초 형식 (AA3, 저판·말뚝). 없으면 저판·말뚝을 만들지 않는다. */
foundationBottomEl?: number;
pileLengthMm?: number;
foundationType?: string;
}
export interface AbutWingModel {
/** 날개벽 선, CAD 각 (deg), 배면 쪽(예제: A1 ≈ 255.32, A2 ≈ 75.32). */
lineDeg: number;
/** 날개벽 선과 거더 방향의 사잇각, 0–90°로 접음(AW01 허용 1°). */
girderDiffDeg: number;
aw01Ok: boolean;
/** 날개벽 경사 (%), (+) = 벽체에서 멀어질수록 높아짐. */
slopePct: number;
/** 날개벽 사이 접속슬래브 폭 (mm) = L − Σ(두께 / |sin Δ| + 날개벽과 간격), 있는 날개벽마다. */
slabWidthMm: number;
wings: { left: WingForm; right: WingForm };
}
export interface AbutBearingModel {
no: number;
/** IP1에서 u 방향 거리 (m). */
s: number;
/** nLeft 방향으로 잰 축에서의 부호 있는 거리 (m). */
lateral: number;
z: number;
seatEl: number;
/** 교좌면에서 패드 상면까지 블럭 높이 (mm), 패드 포함. */
blockHeightMm: number;
/** 받침 방위각 (deg, model-xy). 입력이 없으면 undefined. */
azimuthDeg?: number;
/** 블럭 평면 회전 (deg, −90 … 90): 받침 방위각과 교대축 직각(전면) 방향 사이의 각. 장면에서는 rotateY(이 값). 입력이 없으면 0. */
rotDeg: number;
}
export interface AbutModel {
name: string;
type: AbutType;
/** IP1 (축의 시작점, s = 0). */
ip1: AbutPoint;
/** 축 길이 IP1 → IP3 (m). */
length: number;
u: AbutPoint;
nLeft: AbutPoint;
/** 받침이 축의 nLeft 쪽이면 +1, 반대쪽이면 −1. */
frontSide: 1 | -1;
/** 받침이 1 → n 방향의 왼쪽이면 true (A1 배치). */
frontLeftOfBearingDir: boolean;
/** 입력 순서의 받침. */
bearings: AbutBearingModel[];
/** 축 위 계단 경계(IP1에서 m), 이웃 받침 사이의 중간. */
stepBounds: number[];
/** AW03: 배치가 형식과 맞으면 true. */
layoutMatchesType: boolean;
/** 날개벽 선, 경사, AW01, 접속슬래브 폭. 입력에 skew가 없으면 없다. */
wing?: AbutWingModel;
}
const WING_THICKNESS_MM = 450;
const SLAB_CLEARANCE_MM = 20;
const AW01_TOLERANCE_DEG = 1;
/** 공통 규칙의 패드 두께 (mm). 블럭 높이에 포함된다(패드 포함). */
export const ABUT_PAD_MM = 50;
const sub = (a: AbutPoint, b: AbutPoint): AbutPoint => ({ e: a.e - b.e, n: a.n - b.n });
/**
* 블럭 평면 회전 (deg): the bearing azimuth direction d = (sin az, cos az) seen in the axis frame (s along u, t along the front),
* folded to −90 … 90 because the block is symmetric. 0 when the azimuth is missing.
*/
export function bearingRotDeg(azimuthDeg: number | undefined, u: AbutPoint, front: AbutPoint): number {
if (azimuthDeg === undefined || !Number.isFinite(azimuthDeg)) return 0;
const r = (azimuthDeg * Math.PI) / 180;
const d = { e: Math.sin(r), n: Math.cos(r) };
let phi = (Math.atan2(d.e * u.e + d.n * u.n, d.e * front.e + d.n * front.n) * 180) / Math.PI;
if (phi > 90) phi -= 180;
if (phi < -90) phi += 180;
return phi;
}
const dot = (a: AbutPoint, b: AbutPoint) => a.e * b.e + a.n * b.n;
const cross = (a: AbutPoint, b: AbutPoint) => a.e * b.n - a.n * b.e;
export function buildAbut(input: AbutInput): AbutModel {
if (input.bearings.length < 2) throw new Error(`교대 ${input.name}: 받침이 2개 이상 필요합니다.`);
const axisVec = sub(input.ip3, input.ip1);
const length = Math.hypot(axisVec.e, axisVec.n);
if (!(length > 0)) throw new Error(`교대 ${input.name}: IP1과 IP3이 같은 점입니다.`);
const u = { e: axisVec.e / length, n: axisVec.n / length };
const nLeft = { e: -u.n, n: u.e };
const lateral = input.bearings.map((b) => dot(sub(b.pos, input.ip1), nLeft));
const meanLateral = lateral.reduce((a, x) => a + x, 0) / lateral.length;
const frontSide: 1 | -1 = meanLateral >= 0 ? 1 : -1;
// 1 → n 방향: 첫 받침(①)에서 마지막 받침까지. 전면 = 받침 쪽. 형식과 맞는지 확인한다.
const first = input.bearings[0];
const last = input.bearings[input.bearings.length - 1];
const dir = sub(last.pos, first.pos);
const frontVec = { e: nLeft.e * frontSide, n: nLeft.n * frontSide };
const frontLeftOfBearingDir = cross(dir, frontVec) > 0;
const layoutMatchesType = input.type === 'A1' ? frontLeftOfBearingDir : !frontLeftOfBearingDir;
const minZ = Math.min(...input.bearings.map((b) => b.z));
const bearings: AbutBearingModel[] = input.bearings.map((b, i) => {
const s = dot(sub(b.pos, input.ip1), u);
const seatEl = b.seatEl ?? input.seatEl + (b.z - minZ);
const blockHeightMm = Math.round((b.z - seatEl) * 1000);
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm, azimuthDeg: b.azimuthDeg, rotDeg: bearingRotDeg(b.azimuthDeg, u, frontVec) };
});
const sorted = [...bearings].sort((a, b) => a.s - b.s);
const stepBounds = sorted.slice(1).map((b, i) => (sorted[i].s + b.s) / 2);
const wing = input.skewDeg === undefined ? undefined : buildWing(input, u, nLeft, frontSide, dir, length);
return { name: input.name, type: input.type, ip1: input.ip1, length, u, nLeft, frontSide, frontLeftOfBearingDir, bearings, stepBounds, layoutMatchesType, wing };
}
const deg = (rad: number) => (rad * 180) / Math.PI;
const norm360 = (x: number) => ((x % 360) + 360) % 360;
/** 두 선 방향의 차이(deg), 0–90°로 접는다. 선에는 방향이 없다. */
const lineDiff = (a: number, b: number) => {
const d = norm360(a - b) % 180;
return Math.min(d, 180 - d);
};
function buildWing(input: AbutInput, u: AbutPoint, nLeft: AbutPoint, frontSide: 1 | -1, dir: AbutPoint, length: number): AbutWingModel {
const skew = input.skewDeg!;
const bearingCad = norm360(deg(Math.atan2(dir.n, dir.e)));
// skew만큼 시계방향으로 돌린 선이 날개벽 선이다. 배면 쪽(받침 반대쪽)이 방향을 정한다.
const line = norm360(bearingCad - skew);
const back = { e: -nLeft.e * frontSide, n: -nLeft.n * frontSide };
const radLine = (line * Math.PI) / 180;
const pick = Math.cos(radLine) * back.e + Math.sin(radLine) * back.n > 0 ? line : norm360(line + 180);
const axisCad = norm360(deg(Math.atan2(u.n, u.e)));
const sinDelta = Math.abs(Math.sin(((pick - axisCad) * Math.PI) / 180));
const wings = input.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
// 날개벽 두께는 측별 입력 (2026-11-04, 없으면 450): 축 방향 폭 = 두께 / |sin Δ|.
const thick = { left: input.wingThicknessMm?.left ?? WING_THICKNESS_MM, right: input.wingThicknessMm?.right ?? WING_THICKNESS_MM };
const gap = input.slabWingGapMm ?? SLAB_CLEARANCE_MM;
const present = (f: WingForm, side: 'left' | 'right') => (f === 'none' ? 0 : thick[side] / sinDelta + gap);
const slabWidthMm = Math.round(length * 1000 - present(wings.left, 'left') - present(wings.right, 'right'));
const girderDiffDeg = input.girderAzimuthDeg === undefined ? NaN : lineDiff(pick, input.girderAzimuthDeg);
const slopeI = input.slopeI ?? 0;
return {
lineDeg: pick,
girderDiffDeg,
aw01Ok: Number.isNaN(girderDiffDeg) ? true : girderDiffDeg <= AW01_TOLERANCE_DEG,
slopePct: (input.type === 'A1' ? -slopeI : slopeI) * 100,
slabWidthMm,
wings,
};
}