import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis'; import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block'; import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block'; import { type CopingRebarSettings, copingDefaults, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './rebar/coping-settings'; import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-settings'; import { type FootingRebarSettings, DEFAULT_FOOTING_REBAR } from './rebar/footing-settings'; import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles'; import type { PierInput, Row, SolplatePlan } from './types'; import type { BearingPair } from './bearing-pair'; import { type BearingSole, SOLE_DEFAULTS, structureSoles } from './sole-plate'; import { type CopingGeom, P1_COPING_GEOM } from './coping-geom'; import { castShape } from './cast-in-place-shape'; /** Default Level width (mm) when no input: T형 2100 (P1), 2·3주형 D + 200 (spec §3, 2주형 DWG 2026-11-06). */ export function defaultLevelWidthMm(type: PierInput['type'], columnDiameterMm: number): number { return type === 'T형' ? P1_COPING_GEOM.levelWidth : columnDiameterMm + DIM.multiLevelExtra * 1000; } /** 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 (spec §4.4, 3주형형상배근.md §2.2, 2주형 DWG 2026-11-06): * from each Level end it rises concaveRise over the horizontal haunch length concaveHaunch, stays flat, and comes back down. */ concaveRise: 0.5, concaveHaunch: 1.5, /** Multi-column Level width = D + this (spec §3, 2주형 DWG). T형 keeps levelWidth (= D + 100 for P1). */ multiLevelExtra: 0.2, padThickness: 0.05, padSize: 1.0, blockSize: 1.2, /** W11 (제안 2026-11-10): 받침블럭 콘크리트(패드 제외) 최소 높이 = 피복 50 + D13 = 63 mm (교대 I01과 같은 기준). */ blockMinHeight: 0.063, 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; /** Solplate of this bearing (2026-11-06): S from the girder pair, T per row, T1 on the 오르막 쪽. */ plan: SolplatePlan; /** Block bottom from the 두부보 상면 slope (before any 교좌면 EL input), for the check against the input (2026-11-03). */ seatCalc: 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, 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[]; /** Rear-row steps. Unequal rows use their own coordinate-based boundaries. */ backSegments?: 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; /** Column diameter D (mm) used by the model and the column rebar. */ columnDiameterMm: number; bearings: BearingLocal[]; blocks: BlockModel[]; /** 일체 블록 (승인 2026-10-26): empty unless 일체 is chosen and allowed. The per-bearing blocks stay for the solplate and pads. */ integratedBlocks: IntegratedBlock[]; /** 받침블럭 철근 설정 used by the rebar (승인 2026-10-26). */ blockRebar: BlockRebarSettings; /** 코핑 배근 입력 used by the coping rebar (승인 2026-10-26). */ copingRebar: CopingRebarSettings; /** 상면 주철근 이음 used by the coping rebar (승인 2026-10-26). */ topSplice: TopSpliceSettings; /** 기둥 배근 입력 used by the column rebar (승인 2026-10-26). */ columnRebar: ColumnRebarSettings; /** 확대기초 배근 입력 used by the footing rebar (승인 2026-10-26). */ footingRebar: FootingRebarSettings; 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; }; /** Dedicated rebar source; never substitute footing-anchored column inputs. */ castInput?: PierInput; /** Solplate of every bearing with its S source, pair and warnings (2026-11-06), for the ② card and checks. */ soles: BearingSole[]; } /** * 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. */ /** Bridge context for one pier: the girder pairs over A1, P1 … Pn, A2 (girderPairs), for the solplate slope S. */ export interface PierContext { pairs?: BearingPair[] } export function buildPier(input: PierInput, ctx: PierContext = {}): 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; // Column diameter (m): the input, with 2.0 m when absent (승인 2026-10-23). const colD = (input.columnDiameterMm ?? DIM.columnDiameter * 1000) / 1000; const levelW = (input.levelWidthMm ?? defaultLevelWidthMm(input.type, colD * 1000)) / 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('전열과 후열에 각각 받침이 1개 이상 필요합니다.'); const unequalRows = front.length !== back.length; // 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) })); if (unequalRows) { frontPlaced.sort((a,b)=>a.g-b.g); backPlaced.sort((a,b)=>a.g-b.g); } // 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); // 교좌면 EL per bearing (2026-11-10): when given, it is the coping top of that bearing's step; otherwise z − h1 (z − h2 behind). const seatOf = new Map(input.bearings.map((r) => [r.no, r.seatEl])); const segments: Segment[] = frontPlaced.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: seatOf.get(b.no) ?? b.z - h1 })); // Lateral step (spec §4.3, Q1·Q2 추천안): front and back tops differ by 5 mm or more in some segment. // The back steps break midway between consecutive back bearings (2026-11-10), as the front ones do: back bearings need // not sit opposite the front ones (산외교 P11 ⑥ ↔ ⑦). With equal counts, a back bound less than half a block from the // front bound snaps to it, so slightly staggered rows (P1, ≈ 0.09 m) keep one boundary and no sliver step. const backBound = (k: number) => { const own = (backPlaced[k-1].g + backPlaced[k].g) / 2; return !unequalRows && Math.abs(own - bounds[k]) < DIM.blockSize / 2 ? bounds[k] : own; }; const backSegments: Segment[] = backPlaced.map((b,k) => ({ s0: k === 0 ? 0 : backBound(k), s1: k === backPlaced.length-1 ? L : backBound(k + 1), top: seatOf.get(b.no) ?? b.z-h2, })); const stepBounds = [...new Set([0,L,...segments.slice(1).map(s=>s.s0),...backSegments.slice(1).map(s=>s.s0)])].sort((a,b)=>a-b); const stepSamples = stepBounds.slice(1).map((end,k)=>(stepBounds[k]+end)/2); const lateralStep = stepSamples.some(s => Math.abs(segmentTop(segments,s)-segmentTop(backSegments,s)) >= 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, input.columnEndOffsetMm, input.columnSpacingMm); 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 is 오목형: from each Level end it rises // h_r (500) over the horizontal haunch l_h (1500), stays flat, and comes back down (spec §4.4, 3주형형상배근.md §2.2). 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]; // RB-M01: no flat part left between the two haunches. if (gap <= 2 * DIM.concaveHaunch + 1e-9) { throw new Error(`기둥 사이 간격 ${Math.round(gap * 1000)} mm가 오목형 헌치(${DIM.concaveHaunch * 1000} × 2)보다 짧아 코핑 하면을 만들 수 없습니다 (RB-M01).`); } const raisedEl = levelBottom + DIM.concaveRise; underside.push([zoneStart - DIM.concaveHaunch, raisedEl], [next[1] + DIM.concaveHaunch, raisedEl], [next[1], levelBottom]); }); // Elevation outline in coping coordinates for one row of tops, closed by the shared underside. const outline = (steps: Segment[]): [number, number][] => { const pts: [number, number][] = [[0, endBottom]]; steps.forEach(seg => pts.push([seg.s0, seg.top], [seg.s1, seg.top])); 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 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) { // The front row may lie on either side of the axis (it depends on the IP1 → IP3 direction), so each part takes the // side of its own row; the order stays [front, back]. const frontLow = (fRows.length && bRows.length ? meanN(fRows) - meanN(bRows) : meanN(frontPlaced) - meanN(backPlaced)) <= 0; copingParts.push( { k, nFrom: frontLow ? -planHalfB : nStep, nTo: frontLow ? nStep : planHalfB, profile: shifted(outline(segments), base[k]) }, { k, nFrom: frontLow ? nStep : -planHalfB, nTo: frontLow ? planHalfB : nStep, profile: shifted(outline(backSegments), base[k]) }, ); } else { copingParts.push({ k, nFrom: -planHalfB, nTo: planHalfB, profile: shifted(outline(segments), base[k]) }); } } const fd = footingDims(input); // 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), fd.L)) { footingSets.push({ s: g.s - base[k], k, length: g.length }); } } const cast = castShape(input); if (input.foundationType === '현장타설말뚝' && (!Number.isFinite(cast.pileTopEl) || cast.pileTopEl >= levelBottom || !Number.isFinite(input.pileLengthMm) || input.pileLengthMm <= 0)) throw new Error('현장타설말뚝은 지반고가 코핑 Level 하면보다 낮고 말뚝 길이가 양수여야 합니다.'); const footingTop = input.foundationType === '현장타설말뚝' ? cast.pileTopEl : input.footingBottomEl + fd.H; const columns: ColumnModel[] = colG.map((g, i) => ({ s: g - base[colK[i]], k: colK[i], top: levelBottom, bottom: footingTop, diameter: colD, })); const footings: FootingModel[] = []; const blindings: BlindingModel[] = []; const piles: PileModel[] = []; const pileLength = input.pileLengthMm / 1000; const pileTip = input.foundationType === '현장타설말뚝' ? cast.pileBottomEl : input.footingBottomEl - pileLength; if (input.foundationType === '현장타설말뚝') { columns.forEach((c) => { piles.push({ s: c.s, k: c.k, n: 0, top: cast.pileTopEl, bottom: pileTip, diameter: colD }); }); } else { for (const f of footingSets) { footings.push({ s: f.s, k: f.k, top: footingTop, bottom: input.footingBottomEl, length: f.length, width: fd.W }); blindings.push({ s: f.s, k: f.k, top: input.footingBottomEl, length: f.length + fd.blM, width: fd.W + fd.blM, thickness: fd.blT, }); if (input.foundationType === '말뚝기초') { const g = pileGeometry(f.length, fd.W); const counts = resolveRowCounts(input.pileRows, g); piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, g, counts, fd.pileD).map((p) => ({ ...p, k: f.k }))); } } } const blocks: BlockModel[] = []; // 계산 방위각을 적용한 받침 (2026-11-06): that value turns the block, bearing and solplate; the workbook value stays in input. const azimuthOf = (r: { no: number; azimuthDeg: number }) => input.azimuthApplied?.[String(r.no)] ?? r.azimuthDeg; const bearingAz = new Map(input.bearings.map((r) => [r.no, azimuthOf(r)])); const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u); // Solplate of every bearing (결정 2026-11-06): S from the girder pair (ctx.pairs over the whole bridge), else the // 종단경사 input, else the other row; T per row; the girder underside stays, so the block top drops by T − T_ref. // 받침높이 per bearing (2026-11-10): h3 = H − T_ref keeps the girder underside at z + H; a typed h3 still wins. const tRef = input.sole?.tRefMm ?? SOLE_DEFAULTS.tRefMm; const h3ByNo = Object.fromEntries(input.bearings.filter((b) => b.heightMm !== undefined).map((b) => [String(b.no), b.heightMm! - tRef])); const soles = new Map(structureSoles(input.name, input.bearings.map((b) => ({ no: b.no, row: b.row, azimuthDeg: azimuthOf(b), inputAzimuthDeg: b.azimuthDeg })), { ...input.sole, h3ByNo }, ctx.pairs ?? [], input.longitudinalSlope === undefined ? undefined : input.longitudinalSlope * 100).map((s) => [s.no, s])); const planOf = (no: number, k: number, skewDeg: number): SolplatePlan => { const s = soles.get(no)!; // The block's c axis in (E, N): (−sin d)·u + (cos d)·nrm; T1 goes to the end of c that points uphill. const { u, nrm } = axis.segments[k]; const d = (skewDeg * Math.PI) / 180; const c = { e: -Math.sin(d) * u.e + Math.cos(d) * nrm.e, n: -Math.sin(d) * u.n + Math.cos(d) * nrm.n }; const t1Side = s.uphill && c.e * s.uphill.e + c.n * s.uphill.n > 0 ? 1 : -1; return { L: s.L, W: s.W, slopePct: s.slopePct, T: s.T, T1: s.T1, T2: s.T2, h3: s.h3, t1Side }; }; const dropOf = (no: number) => soles.get(no)!.dropMm / 1000; // 교좌면 EL (2026-11-03): when the input gives it, the block bottom is that EL; otherwise the 두부보 상면 slope. // seatCalc stays the slope value (z − h1, z − h2) as the W10 reference. frontPlaced.forEach((b) => { const seatCalc = b.z - h1; const bottom = seatOf.get(b.no) ?? seatCalc; const skew = skewOf(b.no, b.k), drop = dropOf(b.no); blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness - drop, padTop: b.z - drop, rowSkewDeg: skew, plan: planOf(b.no, b.k, skew), seatCalc }); }); backPlaced.forEach((b) => { const seatCalc = b.z - h2; const bottom = seatOf.get(b.no) ?? seatCalc; const skew = skewOf(b.no, b.k), drop = dropOf(b.no); blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness - drop, padTop: b.z - drop, rowSkewDeg: skew, plan: planOf(b.no, b.k, skew), seatCalc }); }); 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 = unequalRows ? stepSamples.map(s=>segmentTop(segments,s)-segmentTop(backSegments,s)) : 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, backSegments, 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 }, columnDiameterMm: colD * 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, integratedBlocks: buildIntegratedBlocks(blocks, input.bearingLayout === '일체'), blockRebar: { ...DEFAULT_BLOCK_REBAR, ...input.blockRebar }, copingRebar: { ...copingDefaults(columns.length), ...input.copingRebar }, topSplice: { ...DEFAULT_TOP_SPLICE, ...input.topSplice }, columnRebar: { ...DEFAULT_COLUMN_REBAR, ...input.columnRebar }, footingRebar: { ...DEFAULT_FOOTING_REBAR, ...input.footingRebar }, columns, footings, blindings, piles, groundEl: input.groundEl, derived: { columnHeight: levelBottom - columns[0].bottom, soilCover: input.foundationType === '현장타설말뚝' ? 0 : input.groundEl - footingTop, pileCount: piles.length, pileLayout: input.foundationType === '말뚝기초' ? layoutText(resolveRowCounts(input.pileRows, pileGeometry(footings[0].length, fd.W)), pileGeometry(footings[0].length, fd.W)) : null, pileTipEl: piles.length ? pileTip : null, blockHeightFront, blockHeightBack, frontBackDiffSpread, }, soles: [...soles.values()], ...(input.foundationType === '현장타설말뚝' ? { castInput: { ...input } } : {}), }; } /** * Column centres along the coping (m). spec §4.5: L/5 · (L/2) · 4L/5. For 2·3주형 the 끝 여유 e_c and 간격 s_c may be typed * (Q-M10 미결, 3주형형상배근.md §2.1): e_c + k·s_c, s_c defaulting to the even spacing between e_c and L − e_c. */ export function columnPositions(type: PierInput['type'], L: number, endOffsetMm?: number, spacingMm?: number): number[] { if (type === 'T형') return [L / 2]; const n = type === '2주형' ? 2 : 3; if (endOffsetMm !== undefined) { const e = endOffsetMm / 1000; const step = spacingMm !== undefined ? spacingMm / 1000 : (L - 2 * e) / (n - 1); return Array.from({ length: n }, (_, k) => e + k * step); } 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[], footL: number = DIM.footingL): { 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] < footL) 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] + footL, })); } /** * 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, footingWidth: number = DIM.footingW): PileGeometry { return { footingLength, footingWidth, end: DIM.pileEnd, minSpacing: DIM.pileSpacing }; } /** Footing and pile dimensions in m: the input when given, else the 1단계 constants (승인 2026-10-23). */ export function footingDims(input: PierInput): { L: number; W: number; H: number; blM: number; blT: number; pileD: number } { return { L: (input.footingLengthMm ?? DIM.footingL * 1000) / 1000, W: (input.footingWidthMm ?? DIM.footingW * 1000) / 1000, H: (input.footingHeightMm ?? DIM.footingH * 1000) / 1000, // 비림 여유 is the margin on each side (삽도 한쪽 100); the blinding grows by twice that on each length and width. blM: (2 * (input.blindingMarginMm ?? DIM.blindingMargin * 500)) / 1000, blT: (input.blindingThicknessMm ?? DIM.blindingT * 1000) / 1000, pileD: (input.pileOuterMm ?? DIM.pileDiameter * 1000) / 1000, }; } /** * 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[], diameter: number = DIM.pileDiameter): 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, }); } }); return out; }