The three values are inputs on the coping card, with the P1 values (1000 · 1500 · 2100) when absent, and they are tracked like the other shape inputs. The geometry and the coping rebar read them from one CopingGeom: the U heights follow the closed form (h1 − 2c) + h2·min(x_s, x_L0)/x_L0, which reproduces the P1 values. The side rows are assumed to keep their position relative to the Level underside (명세 확인 필요). P1 results are unchanged; the full suite passes. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
522 lines
20 KiB
TypeScript
522 lines
20 KiB
TypeScript
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
|
||
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
|
||
import type { PierInput, Row } from './types';
|
||
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
||
|
||
/** 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;
|
||
}
|
||
|
||
/** Stretch k, local s from that stretch's base, g = coping-relative arc length, n = lateral offset. */
|
||
export interface BearingLocal {
|
||
no: number;
|
||
row: Row;
|
||
k: number;
|
||
s: number;
|
||
g: number;
|
||
n: number;
|
||
z: number;
|
||
}
|
||
|
||
export interface Stretch {
|
||
origin: Vec2;
|
||
u: Vec2;
|
||
nrm: Vec2;
|
||
/** Coping-relative arc length where this stretch starts (0 for k = 0, the kink for k = 1). */
|
||
base: number;
|
||
}
|
||
|
||
export interface BlockModel {
|
||
no: number;
|
||
row: Row;
|
||
k: number;
|
||
s: number;
|
||
g: number;
|
||
n: number;
|
||
bottom: number;
|
||
top: number;
|
||
padTop: number;
|
||
/** Skew (deg, −90…90] of the bearing row against the perpendicular of the stretch axis; the block sides follow the row. */
|
||
rowSkewDeg: number;
|
||
}
|
||
|
||
/** Skew of a bearing row (deg) from the bearing azimuth and the stretch axis direction u (see {@link BlockModel.rowSkewDeg}). */
|
||
export function rowSkewDeg(bearingAzimuthDeg: number, u: Vec2): number {
|
||
if (Number.isNaN(bearingAzimuthDeg)) return 0;
|
||
const axisAz = (Math.atan2(u.e, u.n) * 180) / Math.PI;
|
||
const d = (((bearingAzimuthDeg - (axisAz + 90)) % 180) + 540) % 180; // 0…180
|
||
return d > 90 ? d - 180 : d;
|
||
}
|
||
|
||
/**
|
||
* (s, n) of a point (a along the block's s side, c along its n side) measured from the block centre, after the block turns
|
||
* by its row skew. Same turn as the 3D geometry (rotateY(−skew)).
|
||
*/
|
||
export function blockFramePoint(b: Pick<BlockModel, 's' | 'n' | 'rowSkewDeg'>, a: number, c: number): [number, number] {
|
||
const d = (b.rowSkewDeg * Math.PI) / 180;
|
||
return [b.s + a * Math.cos(d) - c * Math.sin(d), b.n + a * Math.sin(d) + c * Math.cos(d)];
|
||
}
|
||
|
||
export interface ColumnModel {
|
||
s: number;
|
||
k: number;
|
||
top: number;
|
||
bottom: number;
|
||
diameter: number;
|
||
}
|
||
|
||
export interface FootingModel {
|
||
s: number;
|
||
k: number;
|
||
top: number;
|
||
bottom: number;
|
||
length: number;
|
||
width: number;
|
||
}
|
||
|
||
export interface BlindingModel {
|
||
s: number;
|
||
k: number;
|
||
top: number;
|
||
length: number;
|
||
width: number;
|
||
thickness: number;
|
||
}
|
||
|
||
export interface PileModel {
|
||
s: number;
|
||
k: number;
|
||
n: number;
|
||
top: number;
|
||
bottom: number;
|
||
diameter: number;
|
||
}
|
||
|
||
/** One coping slab across [nFrom, nTo]; profile is its (s, EL) outline, closed. */
|
||
export interface CopingPart {
|
||
k: number;
|
||
nFrom: number;
|
||
nTo: number;
|
||
profile: [number, 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 parts per stretch, split across the width by the lateral step line (one part per stretch without a step). */
|
||
copingParts: CopingPart[];
|
||
stretches: Stretch[];
|
||
/** Bend point IP4, or null for a straight axis. */
|
||
kink: Vec2 | null;
|
||
/** Mitre normal at the kink (unit). A point is on stretch 0 when (p − kink)·mitre ≤ 0. */
|
||
mitre: Vec2 | null;
|
||
levelBottom: number;
|
||
endBottom: number;
|
||
/** Coping heights and Level width in mm, as used by the coping rebar (1단계). */
|
||
copingGeom: CopingGeom;
|
||
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;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Bend point IP4 (spec §4.1-2): the point where the IP1→IP2 line meets the line through IP3 whose
|
||
* direction is IP1→IP2 turned by the bend angle. Positive angle = right (clockwise) turn when travelling IP1→IP2.
|
||
* Returns null for a zero angle, where IP4 is not defined.
|
||
*/
|
||
export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec2 | null {
|
||
if (angleDeg === 0) return null;
|
||
const cross = (a: Vec2, b: Vec2) => a.e * b.n - a.n * b.e;
|
||
const len = Math.hypot(ip2.e - ip1.e, ip2.n - ip1.n);
|
||
const d1: Vec2 = { e: (ip2.e - ip1.e) / len, n: (ip2.n - ip1.n) / len };
|
||
const rad = (-angleDeg * Math.PI) / 180; // clockwise is positive, so the counter-clockwise rotation uses −θ
|
||
const d2: Vec2 = { e: d1.e * Math.cos(rad) - d1.n * Math.sin(rad), n: d1.e * Math.sin(rad) + d1.n * Math.cos(rad) };
|
||
const t = cross({ e: ip3.e - ip1.e, n: ip3.n - ip1.n }, d2) / cross(d1, d2);
|
||
return { e: ip1.e + t * d1.e, n: ip1.n + t * d1.n };
|
||
}
|
||
|
||
/**
|
||
* Builds the first-stage model of a T-type pier from one input record. Pure function.
|
||
* Coordinates: g = arc length from the coping start (BP1), so g = 0 at BP1 and g = L at BP2.
|
||
* Each object carries its stretch k and a local s measured from that stretch's base (k = 0: BP1, k = 1: IP4).
|
||
* A bent coping (IP4 angle ≠ 0) puts each footing in its stretch's frame; footings that would cross the kink are not combined.
|
||
*/
|
||
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');
|
||
const bent = ip4.angleDeg !== 0;
|
||
|
||
const L = input.copingLengthMm / 1000;
|
||
const B = input.copingWidthMm / 1000;
|
||
// Coping heights and Level width (m): inputs, with the P1 values when absent (1단계).
|
||
const copingH1 = (input.copingH1Mm ?? P1_COPING_GEOM.h1) / 1000;
|
||
const copingH2 = (input.copingH2Mm ?? P1_COPING_GEOM.h2) / 1000;
|
||
const levelW = (input.levelWidthMm ?? P1_COPING_GEOM.levelWidth) / 1000;
|
||
const planHalfB = B / 2;
|
||
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
|
||
const ip2v: Vec2 = { e: ip2.x, n: ip2.y };
|
||
const ip3v: Vec2 = { e: ip3.x, n: ip3.y };
|
||
const kink = bent ? solveIp4(ip1v, ip2v, ip3v, ip4.angleDeg) : null;
|
||
const axis = buildAxis(ip1v, ip3v, kink);
|
||
if (kink) {
|
||
// The bend must move the axis forward: the kink ahead of IP1 along IP1→IP2, and IP3 ahead of the kink along the turned direction.
|
||
const d1 = { e: ip2v.e - ip1v.e, n: ip2v.n - ip1v.n };
|
||
const len1 = Math.hypot(d1.e, d1.n);
|
||
const rad = (-ip4.angleDeg * Math.PI) / 180;
|
||
const d2 = { e: (d1.e * Math.cos(rad) - d1.n * Math.sin(rad)) / len1, n: (d1.e * Math.sin(rad) + d1.n * Math.cos(rad)) / len1 };
|
||
const kinkAhead = (kink.e - ip1v.e) * d1.e + (kink.n - ip1v.n) * d1.n > 0;
|
||
const ip3Ahead = (ip3v.e - kink.e) * d2.e + (ip3v.n - kink.n) * d2.n > 0;
|
||
if (!kinkAhead || !ip3Ahead) {
|
||
throw new Error('꺾임각과 IP 배치가 맞지 않습니다. IP4가 축 뒤로 넘어갑니다. IP3이 꺾인 방향의 앞쪽에 있는지 확인하세요.');
|
||
}
|
||
}
|
||
const lengthIp = axis.length;
|
||
const sStart = copingSpan(axis, L).sStart;
|
||
const arcA = kinkArc(axis);
|
||
const kinkG = arcA === null ? null : arcA - sStart;
|
||
if (kinkG !== null && (kinkG <= 0 || kinkG >= L)) {
|
||
throw new Error('꺾임점이 코핑 범위 밖에 있습니다. 코핑 길이를 확인하세요.');
|
||
}
|
||
const base = [0, kinkG ?? 0];
|
||
const { u, nrm } = axis.segments[0];
|
||
const azimuthDeg = (((Math.atan2(u.e, u.n) * 180) / Math.PI) + 360) % 360;
|
||
|
||
// IP2 is a 1:9 reference point on a straight axis (intent.PR, spec §2.2). On a bent axis IP2 is on the first stretch, so no check.
|
||
const ip2Calc: Vec2 = kink ? ip2v : { e: ip1.x + 0.1 * (ip3.x - ip1.x), n: ip1.y + 0.1 * (ip3.y - ip1.y) };
|
||
const ip2Input: Vec2 = ip2v;
|
||
|
||
const pointAt = (g: number, n: number): Vec2 => {
|
||
const arc = sStart + g;
|
||
const k = kinkG !== null && g > kinkG ? 1 : 0;
|
||
return fromAxis(axis, { k, s: arc, n });
|
||
};
|
||
const bp1 = pointAt(0, 0);
|
||
const bp2 = pointAt(L, 0);
|
||
const pivot = pointAt(L / 2, 0);
|
||
|
||
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 place = (b: { x: number; y: number }) => {
|
||
const q = toAxis(axis, { e: b.x, n: b.y });
|
||
const g = q.s - sStart;
|
||
return { k: q.k, g, s: g - base[q.k], n: q.n };
|
||
};
|
||
const frontPlaced = front.map((b) => ({ no: b.no, row: '전열' as Row, z: b.z, ...place(b) }));
|
||
const backPlaced = back.map((b) => ({ no: b.no, row: '후열' as Row, z: b.z, ...place(b) }));
|
||
|
||
// Step boundaries sit midway between consecutive front bearings in coping coordinates (spec §4.3).
|
||
const bounds = [0];
|
||
for (let k = 0; k < frontPlaced.length - 1; k++) {
|
||
bounds.push((frontPlaced[k].g + frontPlaced[k + 1].g) / 2);
|
||
}
|
||
bounds.push(L);
|
||
const segments: Segment[] = frontPlaced.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: b.z - h1 }));
|
||
|
||
// Lateral step (spec §4.3, Q1·Q2 추천안): front and back tops differ by 5 mm or more in some segment.
|
||
const backTops = backPlaced.map((b) => b.z - h2);
|
||
const lateralStep = backTops.some((top, k) => Math.abs(segments[k].top - top) >= DIM.lateralTolerance);
|
||
|
||
// Columns (spec §4.5). Positions are coping coordinates; a column must stay a Level half-width away from the kink.
|
||
const colG = columnPositions(input.type, L);
|
||
if (kinkG !== null) {
|
||
const near = nearKink(axis, colG.map((g) => sStart + g), levelW / 2);
|
||
if (near.length) throw new Error('미실시: 꺾임점 근처(Level 반폭 이내)에 기둥이 있습니다. 기둥 위치를 확인하세요.');
|
||
}
|
||
const colK = colG.map((g) => (kinkG !== null && g > kinkG ? 1 : 0));
|
||
// Coping underside: ends at (top − end height h1 = 1000), Level zone at (top − h1 − h2 taper height = 1500).
|
||
const levelBottom = input.copingTopEl - copingH1 - copingH2;
|
||
const endBottom = input.copingTopEl - copingH1;
|
||
const levelStart = Math.min(...colG) - levelW / 2;
|
||
const levelEnd = Math.max(...colG) + levelW / 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 = [...colG].sort((a, b) => b - a).map((s) => [s - levelW / 2, s + levelW / 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]);
|
||
});
|
||
|
||
// Elevation outline in coping coordinates for one row of tops, closed by the shared underside.
|
||
const outline = (tops: number[]): [number, number][] => {
|
||
const pts: [number, number][] = [[0, endBottom]];
|
||
segments.forEach((seg, k) => pts.push([seg.s0, tops[k]], [seg.s1, tops[k]]));
|
||
pts.push(...underside);
|
||
return pts;
|
||
};
|
||
// Each stretch uses the same outline shifted to its own base; the mitre plane trims the overlap at the kink.
|
||
const shifted = (pts: [number, number][], b: number): [number, number][] => pts.map(([s, z]) => [s - b, z]);
|
||
|
||
const meanN = (rows: { n: number }[]) => rows.reduce((sum, r) => sum + r.n, 0) / rows.length;
|
||
const frontTops = segments.map((seg) => seg.top);
|
||
const copingParts: CopingPart[] = [];
|
||
const stretchCount = kinkG === null ? 1 : 2;
|
||
for (let k = 0; k < stretchCount; k++) {
|
||
const fRows = frontPlaced.filter((b) => b.k === k);
|
||
const bRows = backPlaced.filter((b) => b.k === k);
|
||
const nStep = fRows.length && bRows.length
|
||
? (meanN(fRows) + meanN(bRows)) / 2
|
||
: (meanN(frontPlaced) + meanN(backPlaced)) / 2;
|
||
if (lateralStep) {
|
||
copingParts.push(
|
||
{ k, nFrom: -planHalfB, nTo: nStep, profile: shifted(outline(frontTops), base[k]) },
|
||
{ k, nFrom: nStep, nTo: planHalfB, profile: shifted(outline(backTops), base[k]) },
|
||
);
|
||
} else {
|
||
copingParts.push({ k, nFrom: -planHalfB, nTo: planHalfB, profile: shifted(outline(frontTops), base[k]) });
|
||
}
|
||
}
|
||
|
||
// Footings are grouped per stretch so that a 일체기초 never crosses the kink; each group is aligned with its stretch.
|
||
const footingSets: { s: number; k: number; length: number }[] = [];
|
||
for (let k = 0; k < stretchCount; k++) {
|
||
for (const g of footingGroups(colG.filter((_, i) => colK[i] === k))) {
|
||
footingSets.push({ s: g.s - base[k], k, length: g.length });
|
||
}
|
||
}
|
||
|
||
const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH;
|
||
const columns: ColumnModel[] = colG.map((g, i) => ({
|
||
s: g - base[colK[i]],
|
||
k: colK[i],
|
||
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;
|
||
|
||
if (input.foundationType === '현장타설말뚝') {
|
||
columns.forEach((c) => {
|
||
piles.push({ s: c.s, k: c.k, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter });
|
||
});
|
||
} else {
|
||
for (const f of footingSets) {
|
||
footings.push({ s: f.s, k: f.k, top: footingTop, bottom: input.footingBottomEl, length: f.length, width: DIM.footingW });
|
||
blindings.push({
|
||
s: f.s,
|
||
k: f.k,
|
||
top: input.footingBottomEl,
|
||
length: f.length + DIM.blindingMargin,
|
||
width: DIM.footingW + DIM.blindingMargin,
|
||
thickness: DIM.blindingT,
|
||
});
|
||
if (input.foundationType === '말뚝기초') {
|
||
const g = pileGeometry(f.length);
|
||
const counts = resolveRowCounts(input.pileRows, g);
|
||
piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, g, counts).map((p) => ({ ...p, k: f.k })));
|
||
}
|
||
}
|
||
}
|
||
|
||
const blocks: BlockModel[] = [];
|
||
const bearingAz = new Map(input.bearings.map((r) => [r.no, r.azimuthDeg]));
|
||
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
|
||
frontPlaced.forEach((b) => {
|
||
const bottom = segmentTop(segments, b.g);
|
||
blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) });
|
||
});
|
||
backPlaced.forEach((b) => {
|
||
const bottom = b.z - h2;
|
||
blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) });
|
||
});
|
||
|
||
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 = frontPlaced.map((f, k) => f.z - backPlaced[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,
|
||
copingL: L,
|
||
copingB: B,
|
||
copingRefTop: input.copingTopEl,
|
||
h1,
|
||
h2,
|
||
segments,
|
||
copingParts,
|
||
stretches: axis.segments.map((seg, k) => ({ origin: k === 0 ? bp1 : seg.start, u: seg.u, nrm: seg.nrm, base: base[k] })),
|
||
kink,
|
||
mitre: axis.mitre,
|
||
levelBottom,
|
||
endBottom,
|
||
copingGeom: { h1: copingH1 * 1000, h2: copingH2 * 1000, levelWidth: levelW * 1000 },
|
||
bearings: [...frontPlaced, ...backPlaced].map((b) => ({ no: b.no, row: b.row, k: b.k, s: b.s, g: b.g, 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 === '말뚝기초' ? layoutText(resolveRowCounts(input.pileRows, pileGeometry(footings[0].length)), pileGeometry(footings[0].length)) : 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;
|
||
}
|
||
|
||
/**
|
||
* Footings (spec §4.6). Adjacent columns closer than one footing length share a 일체기초 whose length is
|
||
* the distance between its outer columns + DIM.footingL, centred between them. Other columns get their own footing.
|
||
*/
|
||
export function footingGroups(colS: number[]): { s: number; length: number }[] {
|
||
const groups: number[][] = [];
|
||
for (const s of [...colS].sort((a, b) => a - b)) {
|
||
const last = groups[groups.length - 1];
|
||
if (last && s - last[last.length - 1] < DIM.footingL) last.push(s);
|
||
else groups.push([s]);
|
||
}
|
||
return groups.map((g) => ({
|
||
s: (g[0] + g[g.length - 1]) / 2,
|
||
length: g[g.length - 1] - g[0] + DIM.footingL,
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
/** Pile geometry of one footing, from the 1단계 constants (spec §4.6). */
|
||
export function pileGeometry(footingLength: number): PileGeometry {
|
||
return { footingLength, footingWidth: DIM.footingW, end: DIM.pileEnd, minSpacing: DIM.pileSpacing };
|
||
}
|
||
|
||
/**
|
||
* Piles under one footing (spec §4.6): one 열 per row across the width, each with its own count along the length.
|
||
* counts holds the 본수 of each 열 (see core/piles.ts).
|
||
*/
|
||
export function pilesFor(s: number, topEl: number, length: number, g: PileGeometry, counts: number[]): PileModel[] {
|
||
const rows = rowLayout(counts, g);
|
||
const spacingN = (g.footingWidth - 2 * g.end) / (counts.length - 1);
|
||
const out: PileModel[] = [];
|
||
rows.forEach((row, j) => {
|
||
for (let i = 0; i < row.count; i++) {
|
||
out.push({
|
||
s: s + (i - (row.count - 1) / 2) * row.spacing,
|
||
k: 0,
|
||
n: (j - (counts.length - 1) / 2) * spacingN,
|
||
top: topEl,
|
||
bottom: topEl - length,
|
||
diameter: DIM.pileDiameter,
|
||
});
|
||
}
|
||
});
|
||
return out;
|
||
}
|