- Multi-column coping (2·3주형): underside between columns drops 300 below Level with 300 × 300 haunches (spec §4.5, Q3 추천안). - Steel pipe pile diameter 609.6 mm (spec §4.5, Q5 추천안). - Direct foundation keeps the 100 mm blinding concrete (Q4 추천안, test added). - W03: bearing centre closer than half a block width to a step boundary. - W04: block footprint outside the coping plan. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
import type { PierInput, Row } from './types';
|
||
|
||
/** Dimensions in m. Sources: intent.PR sheet (P1 rules) and spec.md §3 defaults. */
|
||
export const DIM = {
|
||
columnDiameter: 2.0,
|
||
copingH1: 1.0,
|
||
copingH2: 1.5,
|
||
levelWidth: 2.1,
|
||
/** Multi-column coping: underside between columns sits this far below Level, with 45° haunches of the same size (spec §4.5, Q3 추천안). */
|
||
haunch: 0.3,
|
||
padThickness: 0.05,
|
||
padSize: 1.0,
|
||
blockSize: 1.2,
|
||
footingL: 10.4,
|
||
footingW: 8.15,
|
||
footingH: 2.0,
|
||
blindingMargin: 0.2,
|
||
blindingT: 0.1,
|
||
/** 강관 609.6 × 12 (spec §4.5 강관 type, Q5 추천안 적용). */
|
||
pileDiameter: 0.6096,
|
||
pileEnd: 0.7,
|
||
pileSpacing: 2.25,
|
||
/** Front/back EL difference below this is treated as no lateral step (spec §4.3, Q2). */
|
||
lateralTolerance: 0.005,
|
||
} as const;
|
||
|
||
export interface Vec2 {
|
||
e: number;
|
||
n: number;
|
||
}
|
||
|
||
export interface Segment {
|
||
s0: number;
|
||
s1: number;
|
||
top: number;
|
||
}
|
||
|
||
export interface BearingLocal {
|
||
no: number;
|
||
row: Row;
|
||
s: number;
|
||
n: number;
|
||
z: number;
|
||
}
|
||
|
||
export interface BlockModel {
|
||
no: number;
|
||
row: Row;
|
||
s: number;
|
||
n: number;
|
||
bottom: number;
|
||
top: number;
|
||
padTop: number;
|
||
}
|
||
|
||
export interface ColumnModel {
|
||
s: number;
|
||
top: number;
|
||
bottom: number;
|
||
diameter: number;
|
||
}
|
||
|
||
export interface FootingModel {
|
||
s: number;
|
||
top: number;
|
||
bottom: number;
|
||
length: number;
|
||
width: number;
|
||
}
|
||
|
||
export interface BlindingModel {
|
||
s: number;
|
||
top: number;
|
||
length: number;
|
||
width: number;
|
||
thickness: number;
|
||
}
|
||
|
||
export interface PileModel {
|
||
s: number;
|
||
n: number;
|
||
top: number;
|
||
bottom: number;
|
||
diameter: number;
|
||
}
|
||
|
||
export interface PierModel {
|
||
name: string;
|
||
lengthIp: number;
|
||
azimuthDeg: number;
|
||
/** Unit vector along the pier axis (east, north). */
|
||
u: Vec2;
|
||
/** Unit vector across the axis: (u.n, -u.e). Front row is negative, back row positive. */
|
||
nrm: Vec2;
|
||
ip1: Vec2;
|
||
ip2Input: Vec2;
|
||
ip2Calc: Vec2;
|
||
dIp2: number;
|
||
bp1: Vec2;
|
||
bp2: Vec2;
|
||
/** Horizontal centre of the drawing frame: the centre of the first column. */
|
||
pivot: Vec2;
|
||
copingL: number;
|
||
copingB: number;
|
||
copingRefTop: number;
|
||
h1: number;
|
||
h2: number;
|
||
segments: Segment[];
|
||
/** Coping elevation outline as (s, EL) points, closed. */
|
||
profile: [number, number][];
|
||
levelBottom: number;
|
||
endBottom: number;
|
||
bearings: BearingLocal[];
|
||
blocks: BlockModel[];
|
||
columns: ColumnModel[];
|
||
footings: FootingModel[];
|
||
blindings: BlindingModel[];
|
||
piles: PileModel[];
|
||
groundEl: number;
|
||
derived: {
|
||
columnHeight: number;
|
||
soilCover: number;
|
||
pileCount: number;
|
||
pileLayout: string | null;
|
||
pileTipEl: number | null;
|
||
blockHeightFront: number;
|
||
blockHeightBack: number;
|
||
frontBackDiffSpread: number;
|
||
};
|
||
}
|
||
|
||
function toLocal(p: Vec2, origin: Vec2, u: Vec2, nrm: Vec2): { s: number; n: number } {
|
||
const dx = p.e - origin.e;
|
||
const dy = p.n - origin.n;
|
||
return { s: dx * u.e + dy * u.n, n: dx * nrm.e + dy * nrm.n };
|
||
}
|
||
|
||
/**
|
||
* Builds the first-stage (shape only) model of a T-type pier from one input record.
|
||
* Pure function: same input always gives the same output. Throws for cases not implemented yet.
|
||
*/
|
||
export function buildPier(input: PierInput): PierModel {
|
||
const ip = (name: string) => {
|
||
const p = input.ips.find((q) => q.name === name);
|
||
if (!p) throw new Error(`IP 누락: ${name}`);
|
||
return p;
|
||
};
|
||
const ip1 = ip('IP1');
|
||
const ip2 = ip('IP2');
|
||
const ip3 = ip('IP3');
|
||
const ip4 = ip('IP4');
|
||
if (ip4.angleDeg !== 0) {
|
||
throw new Error('미실시: 꺾인 코핑(IP4 꺾임각 ≠ 0)은 아직 지원하지 않습니다.');
|
||
}
|
||
|
||
const dx = ip3.x - ip1.x;
|
||
const dy = ip3.y - ip1.y;
|
||
const lengthIp = Math.hypot(dx, dy);
|
||
const u: Vec2 = { e: dx / lengthIp, n: dy / lengthIp };
|
||
const nrm: Vec2 = { e: u.n, n: -u.e };
|
||
const azimuthDeg = (((Math.atan2(u.e, u.n) * 180) / Math.PI) + 360) % 360;
|
||
|
||
// IP2 is a reference point at a 1:9 division of IP1–IP3 (intent.PR, spec §2.2).
|
||
const ip2Calc: Vec2 = { e: ip1.x + 0.1 * dx, n: ip1.y + 0.1 * dy };
|
||
const ip2Input: Vec2 = { e: ip2.x, n: ip2.y };
|
||
|
||
const L = input.copingLengthMm / 1000;
|
||
const B = input.copingWidthMm / 1000;
|
||
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
|
||
const bp1: Vec2 = { e: ip1v.e + u.e * (lengthIp / 2 - L / 2), n: ip1v.n + u.n * (lengthIp / 2 - L / 2) };
|
||
const bp2: Vec2 = { e: bp1.e + u.e * L, n: bp1.n + u.n * L };
|
||
|
||
const front = input.bearings.filter((b) => b.row === '전열').sort((a, b) => a.no - b.no);
|
||
const back = input.bearings.filter((b) => b.row === '후열').sort((a, b) => a.no - b.no);
|
||
if (front.length !== back.length) throw new Error('E02: 전열·후열 받침 수가 다릅니다.');
|
||
if (front.length < 2) throw new Error('받침이 2개 미만입니다.');
|
||
|
||
// Bearing block heights (spec §4.3 h1OshoeBlock / h2, pad 50 included). Not the coping end heights (DIM.copingH1/H2).
|
||
const h1 = front[0].z - input.copingTopEl;
|
||
const h2 = h1 + (back[0].z - front[0].z);
|
||
|
||
const frontLocal = front.map((b) => ({ no: b.no, row: '전열' as Row, z: b.z, ...toLocal({ e: b.x, n: b.y }, bp1, u, nrm) }));
|
||
const backLocal = back.map((b) => ({ no: b.no, row: '후열' as Row, z: b.z, ...toLocal({ e: b.x, n: b.y }, bp1, u, nrm) }));
|
||
|
||
// Step boundaries sit midway between consecutive front bearings (spec §4.3).
|
||
const bounds = [0];
|
||
for (let k = 0; k < frontLocal.length - 1; k++) {
|
||
bounds.push((frontLocal[k].s + frontLocal[k + 1].s) / 2);
|
||
}
|
||
bounds.push(L);
|
||
const segments: Segment[] = frontLocal.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: b.z - h1 }));
|
||
|
||
// Lateral step: front and back coping tops must match segment by segment (not supported yet).
|
||
frontLocal.forEach((f, k) => {
|
||
const backTop = backLocal[k].z - h2;
|
||
if (Math.abs(segments[k].top - backTop) > DIM.lateralTolerance) {
|
||
throw new Error('미실시: 횡방향 단차(전열·후열 상면 차 ≥ 5 mm)는 아직 지원하지 않습니다.');
|
||
}
|
||
});
|
||
|
||
// Columns (spec §4.5). Multi-column positions are placed; haunch and separate-footing checks are not.
|
||
const colS = columnPositions(input.type, L);
|
||
// Coping underside: ends at (top − end height h1 = 1000), Level zone at (top − h1 − h2 taper height = 1500).
|
||
const levelBottom = input.copingTopEl - DIM.copingH1 - DIM.copingH2;
|
||
const endBottom = input.copingTopEl - DIM.copingH1;
|
||
const levelStart = Math.min(...colS) - DIM.levelWidth / 2;
|
||
const levelEnd = Math.max(...colS) + DIM.levelWidth / 2;
|
||
if (levelStart < 0 || levelEnd > L) throw new Error('코핑 길이가 Level 구간보다 짧습니다.');
|
||
|
||
// Underside from the right end to the left end. Between columns (2·3주형) it drops 300 below Level with 300 × 300 haunches (spec §4.5, Q3).
|
||
const underside: [number, number][] = [[L, endBottom]];
|
||
const levelZones = [...colS].sort((a, b) => b - a).map((s) => [s - DIM.levelWidth / 2, s + DIM.levelWidth / 2] as const);
|
||
levelZones.forEach(([zoneStart, zoneEnd], k) => {
|
||
underside.push([zoneEnd, levelBottom], [zoneStart, levelBottom]);
|
||
const next = levelZones[k + 1];
|
||
if (!next) return;
|
||
const gap = zoneStart - next[1];
|
||
if (gap <= 2 * DIM.haunch) throw new Error('기둥 사이 간격이 헌치(300 × 2)보다 좁아 코핑 하면을 만들 수 없습니다.');
|
||
const lowerEl = levelBottom - DIM.haunch;
|
||
underside.push([zoneStart - DIM.haunch, lowerEl], [next[1] + DIM.haunch, lowerEl], [next[1], levelBottom]);
|
||
});
|
||
|
||
const profile: [number, number][] = [[0, endBottom]];
|
||
for (const seg of segments) {
|
||
profile.push([seg.s0, seg.top], [seg.s1, seg.top]);
|
||
}
|
||
profile.push(...underside);
|
||
|
||
const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH;
|
||
const columns: ColumnModel[] = colS.map((s) => ({
|
||
s,
|
||
top: levelBottom,
|
||
bottom: footingTop,
|
||
diameter: DIM.columnDiameter,
|
||
}));
|
||
|
||
const footings: FootingModel[] = [];
|
||
const blindings: BlindingModel[] = [];
|
||
const piles: PileModel[] = [];
|
||
const pileLength = input.pileLengthMm / 1000;
|
||
const pileTip = input.footingBottomEl - pileLength;
|
||
|
||
for (const s of colS) {
|
||
if (input.foundationType === '현장타설말뚝') {
|
||
piles.push({ s, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter });
|
||
continue;
|
||
}
|
||
footings.push({ s, top: footingTop, bottom: input.footingBottomEl, length: DIM.footingL, width: DIM.footingW });
|
||
blindings.push({
|
||
s,
|
||
top: input.footingBottomEl,
|
||
length: DIM.footingL + DIM.blindingMargin,
|
||
width: DIM.footingW + DIM.blindingMargin,
|
||
thickness: DIM.blindingT,
|
||
});
|
||
if (input.foundationType === '말뚝기초') {
|
||
piles.push(...pilesFor(s, input.footingBottomEl, pileLength));
|
||
}
|
||
}
|
||
|
||
const blocks: BlockModel[] = [];
|
||
frontLocal.forEach((b) => {
|
||
const bottom = segmentTop(segments, b.s);
|
||
blocks.push({ no: b.no, row: '전열', s: b.s, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z });
|
||
});
|
||
backLocal.forEach((b) => {
|
||
const bottom = b.z - h2;
|
||
blocks.push({ no: b.no, row: '후열', s: b.s, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z });
|
||
});
|
||
|
||
const blockHeightFront = blocks.find((b) => b.row === '전열')!.top - blocks.find((b) => b.row === '전열')!.bottom;
|
||
const blockHeightBack = blocks.find((b) => b.row === '후열')!.top - blocks.find((b) => b.row === '후열')!.bottom;
|
||
const diffs = frontLocal.map((f, k) => f.z - backLocal[k].z);
|
||
const frontBackDiffSpread = Math.max(...diffs) - Math.min(...diffs);
|
||
|
||
return {
|
||
name: input.name,
|
||
lengthIp,
|
||
azimuthDeg,
|
||
u,
|
||
nrm,
|
||
ip1: ip1v,
|
||
ip2Input,
|
||
ip2Calc,
|
||
dIp2: (ip2Calc.e - bp1.e) * u.e + (ip2Calc.n - bp1.n) * u.n,
|
||
bp1,
|
||
bp2,
|
||
pivot: { e: bp1.e + u.e * (L / 2), n: bp1.n + u.n * (L / 2) },
|
||
copingL: L,
|
||
copingB: B,
|
||
copingRefTop: input.copingTopEl,
|
||
h1,
|
||
h2,
|
||
segments,
|
||
profile,
|
||
levelBottom,
|
||
endBottom,
|
||
bearings: [...frontLocal, ...backLocal].map((b) => ({ no: b.no, row: b.row, s: b.s, n: b.n, z: b.z })),
|
||
blocks,
|
||
columns,
|
||
footings,
|
||
blindings,
|
||
piles,
|
||
groundEl: input.groundEl,
|
||
derived: {
|
||
columnHeight: levelBottom - columns[0].bottom,
|
||
soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop),
|
||
pileCount: piles.length,
|
||
pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText() : null,
|
||
pileTipEl: piles.length ? pileTip : null,
|
||
blockHeightFront,
|
||
blockHeightBack,
|
||
frontBackDiffSpread,
|
||
},
|
||
};
|
||
}
|
||
|
||
export function columnPositions(type: PierInput['type'], L: number): number[] {
|
||
if (type === 'T형') return [L / 2];
|
||
if (type === '2주형') return [L / 5, L - L / 5];
|
||
return [L / 5, L / 2, L - L / 5];
|
||
}
|
||
|
||
function segmentTop(segments: Segment[], s: number): number {
|
||
const hit = segments.find((seg) => s >= seg.s0 && s < seg.s1) ?? segments[segments.length - 1];
|
||
return hit.top;
|
||
}
|
||
|
||
/**
|
||
* Piles under one footing (spec §4.6): rows across the footing width, piles along the length,
|
||
* both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m.
|
||
*/
|
||
export function pileGrid(): { countS: number; countN: number; spacingS: number; spacingN: number } {
|
||
const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1;
|
||
const countS = span(DIM.footingL);
|
||
const countN = span(DIM.footingW);
|
||
return {
|
||
countS,
|
||
countN,
|
||
spacingS: (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1),
|
||
spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1),
|
||
};
|
||
}
|
||
|
||
/** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */
|
||
export function pileLayoutText(): string {
|
||
const g = pileGrid();
|
||
return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`;
|
||
}
|
||
|
||
export function pilesFor(s: number, topEl: number, length: number): PileModel[] {
|
||
const { countS, countN, spacingS, spacingN } = pileGrid();
|
||
const out: PileModel[] = [];
|
||
for (let i = 0; i < countS; i++) {
|
||
for (let j = 0; j < countN; j++) {
|
||
out.push({
|
||
s: s + (i - (countS - 1) / 2) * spacingS,
|
||
n: (j - (countN - 1) / 2) * spacingN,
|
||
top: topEl,
|
||
bottom: topEl - length,
|
||
diameter: DIM.pileDiameter,
|
||
});
|
||
}
|
||
}
|
||
return out;
|
||
}
|