- 노출형 (삽도 기준, QA-8 가정): 상면 = 흉벽 상단, 끝은 브라켓 위(끝 두께 = 브라켓 높이), 헌치 길이 기본 2000 - 형식을 고르면 경사가 기본값으로 바뀜 (매립형 하향 4 %, 노출형 종단경사), 경사 타일로 변경 가능 - 길이 · 두께 · 흉벽과 간격 · 날개벽과 간격 입력이 3D, 폭, 날개벽 평면에 반영 - 슬래브 모따기 = 날개벽 헌치 (3D와 평면), 날개벽 없는 측은 skew 교좌면 끝선 - 3D 슬래브를 단면 기준 입체로 다시 만듦, 프로젝트 파일 저장 · 검증 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
212 lines
10 KiB
TypeScript
212 lines
10 KiB
TypeScript
/**
|
||
* 교대 형상 · 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,
|
||
};
|
||
}
|