feat(bearing): 교각 솔플레이트를 2026-11-06 결정으로 (짝 받침 S · 열 T · 거더 하면 고정)
받침방위각_솔플레이트검토.md §3과 시안(Version 110–113)을 따른다 (사용자 결정 2026-11-06: 시안 적용). 11-03 C안(받침높이 H 고정, h3 = H − T, S 입력)을 대체한다. - S: 교량 전체(A1, P1 … Pn, A2) 거더 짝 받침의 z 차 / 수평거리. 없으면 교각 종단경사 입력 → 다른 열 같은 순번 값(가정, WS03) → 0 %(WS03). 받침별 직접 수정(수정됨) - 평면 L = A + 100, W = B + 100 (받침별 A × B, 기본 700 × 640 가정), h3 181 - T = 열마다 max(22, ⌈22 + L·|S|/2⌉), 열 T 직접 수정 가능(T2 < T_min → WS02). T1은 오르막 쪽(3D 쐐기의 두꺼운 끝도 그쪽), T2 반대쪽 - 거더 하면 고정: 블럭 상면 = z − (T − T_ref), T_ref 30 - 짝 받침은 불러오기·프로젝트 열기 때 교량 전체로 계산해 교각 생성에 넘김 - ② 카드: h3·T_ref·T_min·여유·열 T·종단경사 입력, 표에 짝 받침·S 근거·받침별 S/A/B 수정·검산 W08·검사 - P1 검증(A1과 함께): S −1.908 … −1.902 %, L 800 · W 740 · T 30 · T1 38 · T2 22, 블럭 높이 변화 없음 - 블럭·솔플레이트 테스트는 A1–P1 짝 문맥으로 실행 (짝이 없으면 S 0 · T 22 · 블럭 +8 mm) 교대(QA-15 C안)와 Pier 시트 종단경사 열 읽기는 아직 남음. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fec2743cb
commit
a0b0929e13
19 files changed
+486
-109
No files matched your search
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* 거더 짝 받침 · 종단경사 S · 솔플레이트 열 두께 (결정 2026-11-06, RAG/받침방위각_솔플레이트검토.md §3, Q-SH1·4·8·9).
|
||||
* Pure. Not wired into the model yet: the height rule (Q-SH7, 블럭 상면 = z − (T − T_ref)) conflicts with the 2026-11-03
|
||||
* C안 already in use (받침높이 H 고정, h3 = H − T, src/core/solplate-rule.ts), so only the parts both rules need live here.
|
||||
* Pure. Used by src/core/sole-plate.ts (pier solplates, 2026-11-06) through src/core/bridge-pairs.ts.
|
||||
*
|
||||
* Frame: (E, N) in m as in the model (x = E, y = N). Azimuth: the model's 진북 방위각 (deg, clockwise from north), i.e. the
|
||||
* workbook CAD angle θ_CAD converted by 90 − θ_CAD. A girder at azimuth θ runs along (sin θ, cos θ).
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* along 교각축.
|
||||
* - S1·S2: block rebar bars from the model (bearing-block): count and bar length. For 일체 the pair's bars sit on the front row.
|
||||
*/
|
||||
import type { SlopeSource } from './sole-plate';
|
||||
import type { PierModel } from './geometry';
|
||||
import { bearingEdgeDistances, pairEccentricities } from './bearing-layout';
|
||||
import { INTEGRATED_BLOCK } from './integrated-block';
|
||||
@@ -54,12 +55,24 @@ export interface SolplateRow {
|
||||
T: number;
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** Check: T1 − T2 against L·|S|/100 (mm). */
|
||||
/** Check W08: T1 − T2 against L·|S|/100 (mm), within 1 mm (T1 and T2 are rounded to whole mm). */
|
||||
checkLhs: number;
|
||||
checkRhs: number;
|
||||
ok: boolean;
|
||||
/** 짝 받침 (같은 거더), e.g. "A1-①"; absent when none (2026-11-06). */
|
||||
pair?: string;
|
||||
/** Where S comes from: 계산 (짝) / 입력 (종단경사) / 가정 (다른 열) / 수정됨 / 없음. */
|
||||
source: SlopeSource;
|
||||
/** T typed by the user (수정됨) rather than automatic. */
|
||||
tTyped: boolean;
|
||||
/** 받침 평면 A × B (mm). */
|
||||
A: number;
|
||||
B: number;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
const CIRCLED_NO = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳'];
|
||||
|
||||
function barSummary(bars: BarRow[], name: 'S1' | 'S2'): BarSummary | null {
|
||||
const hit = bars.filter((x) => x.name === name);
|
||||
if (hit.length === 0) return null;
|
||||
@@ -112,9 +125,12 @@ export function solplateRows(m: PierModel): SolplateRow[] {
|
||||
.sort((a, b) => a.no - b.no)
|
||||
.map((b) => {
|
||||
const p = b.plan;
|
||||
const s = m.soles.find((x) => x.no === b.no)!;
|
||||
const checkLhs = p.T1 - p.T2;
|
||||
const checkRhs = (p.L * Math.abs(p.slopePct)) / 100;
|
||||
return {
|
||||
pair: s.pair ? `${s.pair.structure}-${CIRCLED_NO[s.pair.no - 1] ?? s.pair.no}` : undefined,
|
||||
source: s.slopeSource, tTyped: s.tTyped, A: s.aMm, B: s.bMm, warnings: s.warnings,
|
||||
no: b.no,
|
||||
S: p.slopePct,
|
||||
L: p.L,
|
||||
@@ -124,7 +140,7 @@ export function solplateRows(m: PierModel): SolplateRow[] {
|
||||
T2: p.T2,
|
||||
checkLhs,
|
||||
checkRhs,
|
||||
ok: Math.abs(checkLhs - checkRhs) < 1e-6,
|
||||
ok: Math.abs(checkLhs - checkRhs) <= 1 + 1e-9,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Girder pairs over the whole bridge (A1, P1 … Pn, A2 — structureOrder) for the solplate slope S (결정 2026-11-06, Q-SH1).
|
||||
* Pure. Bearing coordinates and z cannot be edited on the screens, so the pairs change only when structures are loaded.
|
||||
*/
|
||||
import type { AbutRecord } from './abut/parse';
|
||||
import { structureOrder } from './abut/order';
|
||||
import { type BearingPair, girderPairs, type PairStructure } from './bearing-pair';
|
||||
import type { PierInput } from './types';
|
||||
|
||||
export function bridgePairs(piers: Pick<PierInput, 'name' | 'bearings'>[], abuts: AbutRecord[]): BearingPair[] {
|
||||
const byPier = new Map(piers.map((p) => [p.name, p]));
|
||||
const byAbut = new Map(abuts.map((a) => [a.name, a]));
|
||||
const structures: PairStructure[] = structureOrder(abuts, piers.map((p) => p.name)).map((ref) => {
|
||||
if (ref.kind === 'pier') {
|
||||
const p = byPier.get(ref.name)!;
|
||||
return { name: p.name, bearings: p.bearings.filter((b) => Number.isFinite(b.azimuthDeg)).map((b) => ({ no: b.no, x: b.x, y: b.y, z: b.z, azimuthDeg: b.azimuthDeg })) };
|
||||
}
|
||||
const a = byAbut.get(ref.name)!;
|
||||
return { name: a.name, bearings: a.bearings.filter((b) => b.azimuthDeg !== undefined).map((b) => ({ no: b.no, x: b.pos.e, y: b.pos.n, z: b.z, azimuthDeg: b.azimuthDeg! })) };
|
||||
});
|
||||
return girderPairs(structures);
|
||||
}
|
||||
+28
-7
@@ -6,7 +6,8 @@ import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-s
|
||||
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 { SOLPLATE_RULE_DEFAULT, solplatePlan } from './solplate-rule';
|
||||
import type { BearingPair } from './bearing-pair';
|
||||
import { type BearingSole, structureSoles } from './sole-plate';
|
||||
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
||||
import { castShape } from './cast-in-place-shape';
|
||||
|
||||
@@ -87,7 +88,7 @@ export interface BlockModel {
|
||||
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 (C안, 2026-11-03): the pier's rule, the same for every bearing. */
|
||||
/** 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;
|
||||
@@ -221,6 +222,8 @@ export interface PierModel {
|
||||
};
|
||||
/** 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[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +248,10 @@ export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec
|
||||
* 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 {
|
||||
/** 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}`);
|
||||
@@ -455,19 +461,33 @@ export function buildPier(input: PierInput): PierModel {
|
||||
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);
|
||||
// Solplate of every bearing from the pier's rule (C안, 2026-11-03): the same plan for each bearing.
|
||||
const plan = solplatePlan(input.solplate ?? SOLPLATE_RULE_DEFAULT);
|
||||
// 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.
|
||||
const soles = new Map(structureSoles(input.name, input.bearings.map((b) => ({ no: b.no, row: b.row, azimuthDeg: b.azimuthDeg })),
|
||||
input.sole, 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.
|
||||
const seatOf = new Map(input.bearings.map((r) => [r.no, r.seatEl]));
|
||||
frontPlaced.forEach((b) => {
|
||||
const seatCalc = segmentTop(segments, b.g);
|
||||
const bottom = seatOf.get(b.no) ?? seatCalc;
|
||||
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), plan, 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;
|
||||
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), plan, 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;
|
||||
@@ -526,6 +546,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
blockHeightBack,
|
||||
frontBackDiffSpread,
|
||||
},
|
||||
soles: [...soles.values()],
|
||||
...(input.foundationType === '현장타설말뚝' ? { castInput: { ...input } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 솔플레이트 (결정 2026-11-06, RAG/받침방위각_솔플레이트검토.md §3, spec.md §5.3, 시안 Version 110–113). Pure.
|
||||
* Replaces the 2026-11-03 C안 (받침높이 H 고정, h3 = H − T, S 입력) by the user's decision to follow the 시안:
|
||||
*
|
||||
* - 평면: L = A + 여유 (거더 방향 = 방위각), W = B + 여유 (여유 100). A × B per bearing; absent = 700 × 640 (P1 전열 값, 가정).
|
||||
* - S: 같은 거더 양끝 받침의 z 차 / 수평거리 (girderPairs). 없으면 교각 종단경사 입력 → 같은 구조물 다른 열의 같은 순번 값
|
||||
* ("가정", WS03) → 0 % (WS03). 받침별 직접 수정 = "수정됨".
|
||||
* - T: 열마다 max(T_min, ⌈max(T_min + L·|S|/2)⌉). 직접 고치면 "수정됨", T2 < T_min이면 WS02.
|
||||
* T1 = round(T + L·|S|/2) 오르막 쪽, T2 = round(T − L·|S|/2).
|
||||
* - 높이: 거더 하면 고정 — 블럭 상면 = z − (T − T_ref); 받침 본체 h3 고정 (181, P1 상세도).
|
||||
*/
|
||||
import { type BearingPair, pairOf } from './bearing-pair';
|
||||
|
||||
export interface SoleSettings {
|
||||
/** 받침 본체 높이 h3 (mm). */
|
||||
h3Mm?: number;
|
||||
/** T_ref: the solplate thickness the workbook z assumed (mm). */
|
||||
tRefMm?: number;
|
||||
/** T_min: the thin-end minimum (mm). */
|
||||
tMinMm?: number;
|
||||
/** 솔플 여유 L − A = W − B (mm). */
|
||||
marginMm?: number;
|
||||
/** 받침 평면 A (거더 방향) × B per bearing number (mm). */
|
||||
plates?: Record<string, { aMm: number; bMm: number }>;
|
||||
/** T per row, when typed (mm). Absent = 자동. */
|
||||
tByRow?: Partial<Record<string, number>>;
|
||||
/** S per bearing number, when typed (%). */
|
||||
slopePct?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const SOLE_DEFAULTS = { h3Mm: 181, tRefMm: 30, tMinMm: 22, marginMm: 100, aMm: 700, bMm: 640 } as const;
|
||||
|
||||
export type SlopeSource = '계산' | '입력' | '가정' | '수정됨' | '없음';
|
||||
|
||||
export interface BearingSole {
|
||||
no: number;
|
||||
row: string;
|
||||
pair?: { structure: string; no: number };
|
||||
slopePct: number;
|
||||
slopeSource: SlopeSource;
|
||||
aMm: number;
|
||||
bMm: number;
|
||||
/** L along the girder, W across it (mm). */
|
||||
L: number;
|
||||
W: number;
|
||||
/** Row thickness T and whether it was typed; T1 on the 오르막 쪽, T2 on the other end (mm). */
|
||||
T: number;
|
||||
tTyped: boolean;
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** This bearing's requirement T_min + L·|S|/2 (mm). */
|
||||
tReq: number;
|
||||
h3: number;
|
||||
/** Block top drop T − T_ref (mm, + = the block gets lower). */
|
||||
dropMm: number;
|
||||
/** Unit vector (E, N) towards the 오르막 쪽, or null when S = 0. */
|
||||
uphill: { e: number; n: number } | null;
|
||||
warnings: ('WS01' | 'WS02' | 'WS03' | 'WS04')[];
|
||||
}
|
||||
|
||||
export interface SoleBearing {
|
||||
no: number;
|
||||
row: string;
|
||||
azimuthDeg: number;
|
||||
}
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* Solplates of one structure. `pairs` are the girder pairs of the whole bridge (girderPairs over A1, P1 … Pn, A2);
|
||||
* `fallbackSlopePct` is the structure's 종단경사 input (%), when given.
|
||||
*/
|
||||
export function structureSoles(structure: string, bearings: SoleBearing[], settings: SoleSettings = {}, pairs: BearingPair[] = [], fallbackSlopePct?: number): BearingSole[] {
|
||||
const s = { ...SOLE_DEFAULTS, ...settings };
|
||||
const rows = [...new Set(bearings.map((b) => b.row))];
|
||||
const order = (b: SoleBearing) => bearings.filter((q) => q.row === b.row).indexOf(b);
|
||||
const paired = new Map(bearings.map((b) => [b.no, pairOf(pairs, structure, b.no)]));
|
||||
const pairSlope = (b: SoleBearing) => paired.get(b.no)?.slopePct;
|
||||
|
||||
const slopes = bearings.map((b) => {
|
||||
const typed = settings.slopePct?.[String(b.no)];
|
||||
if (typed !== undefined) return { slopePct: typed, source: '수정됨' as SlopeSource };
|
||||
const own = pairSlope(b);
|
||||
if (own !== undefined) return { slopePct: own, source: '계산' as SlopeSource };
|
||||
if (fallbackSlopePct !== undefined) return { slopePct: fallbackSlopePct, source: '입력' as SlopeSource };
|
||||
// The same position in another row that has a pair (시안: 후열 ⑥ ← 전열 ①).
|
||||
for (const other of rows.filter((r) => r !== b.row)) {
|
||||
const twin = bearings.filter((q) => q.row === other)[order(b)];
|
||||
const v = twin && pairSlope(twin);
|
||||
if (v !== undefined) return { slopePct: v, source: '가정' as SlopeSource };
|
||||
}
|
||||
return { slopePct: 0, source: '없음' as SlopeSource };
|
||||
});
|
||||
|
||||
const plate = (b: SoleBearing) => s.plates?.[String(b.no)] ?? { aMm: s.aMm, bMm: s.bMm };
|
||||
const req = bearings.map((b, i) => s.tMinMm + ((plate(b).aMm + s.marginMm) * Math.abs(slopes[i].slopePct)) / 200);
|
||||
const rowT = new Map(rows.map((r) => {
|
||||
const typed = settings.tByRow?.[r];
|
||||
const auto = Math.max(s.tMinMm, Math.ceil(Math.max(...bearings.map((b, i) => (b.row === r ? req[i] : -Infinity))) - 1e-9));
|
||||
return [r, { T: typed ?? auto, typed: typed !== undefined }];
|
||||
}));
|
||||
|
||||
return bearings.map((b, i) => {
|
||||
const { aMm, bMm } = plate(b);
|
||||
const L = aMm + s.marginMm, W = bMm + s.marginMm;
|
||||
const { slopePct, source } = slopes[i];
|
||||
const { T, typed } = rowT.get(b.row)!;
|
||||
const half = (L * Math.abs(slopePct)) / 200;
|
||||
const T1 = Math.round(T + half), T2 = Math.round(T - half);
|
||||
const pair = paired.get(b.no);
|
||||
const warnings: BearingSole['warnings'] = [...(pair?.warnings ?? [])];
|
||||
if (T2 < s.tMinMm) warnings.push('WS02');
|
||||
if (source === '가정' || source === '없음') warnings.push('WS03');
|
||||
// S runs along the structure order, which is the azimuth direction (girderPairs looks ahead along it).
|
||||
const dir = { e: Math.sin(b.azimuthDeg * DEG), n: Math.cos(b.azimuthDeg * DEG) };
|
||||
const sign = Math.sign(slopePct);
|
||||
return {
|
||||
no: b.no, row: b.row, pair: pair ? (pair.from.structure === structure ? pair.to : pair.from) : undefined,
|
||||
slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: s.h3Mm,
|
||||
dropMm: T - s.tRefMm, uphill: sign ? { e: sign * dir.e, n: sign * dir.n } : null, warnings,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Input problems of the settings, in Korean; empty when they can build solplates. */
|
||||
export function soleSettingsIssues(settings: SoleSettings | undefined): string[] {
|
||||
if (!settings) return [];
|
||||
const out: string[] = [];
|
||||
const positive = (v: unknown, label: string) => {
|
||||
if (v !== undefined && !(typeof v === 'number' && Number.isFinite(v) && v > 0)) out.push(`${label}은(는) 0보다 커야 합니다.`);
|
||||
};
|
||||
positive(settings.h3Mm, '받침 높이 h3');
|
||||
positive(settings.tRefMm, 'T_ref');
|
||||
positive(settings.tMinMm, 'T_min');
|
||||
if (settings.marginMm !== undefined && !(Number.isFinite(settings.marginMm) && settings.marginMm >= 0)) out.push('솔플 여유는 0 이상이어야 합니다.');
|
||||
for (const [no, p] of Object.entries(settings.plates ?? {})) { positive(p.aMm, `받침 ${no} A`); positive(p.bMm, `받침 ${no} B`); }
|
||||
for (const [row, t] of Object.entries(settings.tByRow ?? {})) positive(t, `${row} T`);
|
||||
for (const [no, v] of Object.entries(settings.slopePct ?? {})) if (!Number.isFinite(v)) out.push(`받침 ${no} S는 숫자여야 합니다.`);
|
||||
return out;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 솔플레이트 기하 (승인 2026-10-23, C안 2026-11-03). Pure: the solplate of one bearing, from its block (position, rowSkew)
|
||||
* and the pier's solplate plan. The plan is L across the 교각축 직각 (n) and W along the 교축 (s). The top tapers across L:
|
||||
* the thick end T1 is at the (−) side of the 교각축 직각 direction (n = −L/2), the thin end T2 at (+) (n = +L/2).
|
||||
* 솔플레이트 기하 (승인 2026-10-23, 2026-11-06 결정). Pure: the solplate of one bearing, from its block (position, rowSkew)
|
||||
* and the bearing's solplate plan. L runs along the block's c axis (the girder, by the azimuth) and W along a. The top
|
||||
* tapers across L: the thick end T1 is on the 오르막 쪽 (c = t1Side·L/2), the thin end T2 at the other end.
|
||||
* The underside is the bearing top (block top EL.X + h3). Lengths in m, as in the model.
|
||||
*/
|
||||
import { blockFramePoint, type BlockModel } from './geometry';
|
||||
@@ -29,7 +29,9 @@ export function solplateGeometry(block: BlockModel, plan: SolplatePlan): Solplat
|
||||
const T1 = plan.T1 / 1000;
|
||||
const T2 = plan.T2 / 1000;
|
||||
const bottom = block.padTop + plan.h3 / 1000;
|
||||
const thickAt = (c: number) => T1 + ((T2 - T1) * (c + L / 2)) / L;
|
||||
// T1 at c = t1Side·L/2 (the 오르막 쪽), T2 at the other end.
|
||||
const side = plan.t1Side ?? -1;
|
||||
const thickAt = (c: number) => T1 + ((T2 - T1) * (side * -c + L / 2)) / L;
|
||||
const corners: [number, number, number][] = [];
|
||||
for (const a of [-W / 2, W / 2]) {
|
||||
for (const c of [-L / 2, L / 2]) {
|
||||
|
||||
+10
-3
@@ -1,3 +1,4 @@
|
||||
import type { SoleSettings } from './sole-plate';
|
||||
import type { CopingRebarSettings, TopSpliceSettings } from './rebar/coping-settings';
|
||||
import type { ColumnRebarSettings } from './rebar/column-settings';
|
||||
import type { FootingRebarSettings } from './rebar/footing-settings';
|
||||
@@ -62,8 +63,12 @@ export interface PierInput {
|
||||
pileThicknessMm?: number;
|
||||
/** 말뚝 종류 (규격 목록): 강관 (default) or PHC. */
|
||||
pileKind?: '강관' | 'PHC';
|
||||
/** 솔플레이트 규칙 of this pier (C안, 2026-11-03). Absent = the default (211 · 1000 × 1100 · −0.5 %). */
|
||||
/** Obsolete C안 (2026-11-03) rule: kept so that old project files still open; no longer used (replaced by `sole`, 2026-11-06). */
|
||||
solplate?: SolplateRule;
|
||||
/** 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유, 받침별 A × B, 열 T·받침 S 직접 수정. Absent = 기본값. */
|
||||
sole?: SoleSettings;
|
||||
/** 교각 종단경사 (소수, (−) = 교량진행방향으로 낮아짐; Pier 시트 "종단경사" 열, Q-SH2). 짝 받침이 없을 때 S에 쓴다. */
|
||||
longitudinalSlope?: number;
|
||||
/** 지반·지층 (승인 2026-10-26): 위치, 지하수위 고려와 EL, 지층별 행 (위에서 아래, 행의 EL = 그 층의 하단 EL).
|
||||
* Absent = 육상, 지하수위 없음, 지층 행 없음. */
|
||||
groundPosition?: GroundPosition;
|
||||
@@ -109,15 +114,17 @@ export interface SolplateRule {
|
||||
slopePct: number;
|
||||
}
|
||||
|
||||
/** The solplate every bearing of a pier gets from its rule (mm): T is the centre thickness, h3 = H − T. */
|
||||
/** The solplate of one bearing (mm): T is the row thickness, T1 the 오르막 쪽 end, h3 the bearing body (2026-11-06). */
|
||||
export interface SolplatePlan {
|
||||
L: number;
|
||||
W: number;
|
||||
slopePct: number;
|
||||
T: number;
|
||||
/** Thickness at the (−) side (T1) and the (+) side (T2) of the 교각축 직각 direction. */
|
||||
/** Thickness at the 오르막 쪽 (T1) and the other end (T2). */
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** Side of the block's c axis (L direction) where T1 sits: −1 = c = −L/2, +1 = c = +L/2. Absent = −1. */
|
||||
t1Side?: -1 | 1;
|
||||
/** Bearing body height h3 (mm) between the mortar top and the solplate underside. */
|
||||
h3: number;
|
||||
}
|
||||
|
||||
+111
-39
@@ -119,8 +119,10 @@ import { columnDiameterIssues } from './core/column-check';
|
||||
import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec';
|
||||
import { footingPlanSvg, footingSideSvg } from './view/footing-svg';
|
||||
import { P1_COPING_GEOM } from './core/coping-geom';
|
||||
import { SOLPLATE_MIN_T2_MM, SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
|
||||
import type { SolplateRule } from './core/types';
|
||||
import { SOLE_DEFAULTS, type SoleSettings, soleSettingsIssues } from './core/sole-plate';
|
||||
import { SOLPLATE_MIN_T2_MM } from './core/solplate-rule';
|
||||
import { bridgePairs } from './core/bridge-pairs';
|
||||
import type { BearingPair } from './core/bearing-pair';
|
||||
import { type BarSummary, elevationRows, solplateRows } from './core/bearing-table';
|
||||
|
||||
/** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */
|
||||
@@ -254,6 +256,8 @@ const state = {
|
||||
pending: null as Pending | null,
|
||||
source: { fileName: '', sheets: [] as string[] },
|
||||
history: [] as ProjectFile['history'],
|
||||
/** Girder pairs over the bridge (A1, P1 … Pn, A2) for the solplate S (2026-11-06); recomputed when structures load. */
|
||||
bridgePairs: [] as BearingPair[],
|
||||
/** Snapshot of the inputs at the last save or open. null until the project is first saved. */
|
||||
savedSnapshot: null as string | null,
|
||||
};
|
||||
@@ -396,12 +400,12 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
|
||||
const input = withCopingDefaults(rawInput);
|
||||
const issues: Issue[] = [];
|
||||
let model: PierModel | null = null;
|
||||
// 솔플레이트 규칙 (C안, 2026-11-03): a rule that cannot make a solplate is reported, and no model is built from it.
|
||||
const solIssues = solplateRuleIssues(input.solplate ?? SOLPLATE_RULE_DEFAULT);
|
||||
// 솔플레이트 입력 (2026-11-06): settings that cannot make a solplate are reported, and no model is built from them.
|
||||
const solIssues = soleSettingsIssues(input.sole);
|
||||
for (const message of solIssues) issues.push({ code: 'S01', level: 'E', pier: input.name, message });
|
||||
if (validateInput(input, issues) && solIssues.length === 0) {
|
||||
try {
|
||||
model = buildPier(input);
|
||||
model = buildPier(input, { pairs: state.bridgePairs });
|
||||
validatePier(input, model, issues);
|
||||
for (const message of columnDiameterIssues(input.columnDiameterMm ?? 2000, model)) {
|
||||
issues.push({ code: 'D01', level: 'E', pier: input.name, message });
|
||||
@@ -5116,20 +5120,50 @@ function renderElevationTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
}
|
||||
|
||||
/** 솔플레이트 TABLE (모델값, mm, 2026-11-03): S, L, W, T, T1, T2 and the check T1 − T2 ≈ L·|S|/100. */
|
||||
/**
|
||||
* 솔플레이트 TABLE (모델값, mm, 2026-11-06): 짝 받침, S와 근거, A × B, L, W, T, T1 (오르막 쪽), T2, 검산 W08, 검사.
|
||||
* S, A and B can be typed per bearing (수정됨); clearing the cell goes back to the computed or default value.
|
||||
*/
|
||||
function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
if (!pier.model) return;
|
||||
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm)', 'lbl'));
|
||||
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['No', 'S (%)', 'L (교직)', 'W (교축)', 'T (중심)', 'T1 (−측)', 'T2 (+측)', '검산 T1 − T2 ≈ L·|S|/100']) hr.appendChild(el('th', h));
|
||||
for (const h of ['No', '짝 받침', 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
const settings = pier.draft.sole ?? {};
|
||||
const cellInput = (value: number, typed: boolean, label: string, apply: (v: number | undefined) => void) => {
|
||||
const input = el('input');
|
||||
input.type = 'number'; input.step = 'any'; input.value = String(value); input.setAttribute('aria-label', label);
|
||||
input.style.width = '6.5em';
|
||||
if (typed) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => apply(input.value.trim() === '' ? undefined : Number(input.value)));
|
||||
const td = el('td'); td.appendChild(input); return td;
|
||||
};
|
||||
for (const r of solplateRows(pier.model)) {
|
||||
const key = String(r.no);
|
||||
const tr = el('tr');
|
||||
const cells = [String(r.no), r.S.toFixed(2), String(r.L), String(r.W), String(r.T), r.T1.toFixed(1), r.T2.toFixed(1), `${r.checkLhs.toFixed(1)} ≈ ${r.checkRhs.toFixed(1)} ${r.ok ? '일치' : '불일치'}`];
|
||||
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'));
|
||||
tr.appendChild(cellInput(Math.round(r.S * 1000) / 1000, r.source === '수정됨', `받침 ${r.no} S (%)`, (v) => {
|
||||
const next = { ...(settings.slopePct ?? {}) };
|
||||
if (v === undefined) delete next[key]; else next[key] = v;
|
||||
setSole(pier.input.name, { ...settings, slopePct: next });
|
||||
}));
|
||||
tr.appendChild(el('td', r.source));
|
||||
const plate = settings.plates?.[key];
|
||||
for (const dim of ['aMm', 'bMm'] as const) {
|
||||
tr.appendChild(cellInput(dim === 'aMm' ? r.A : r.B, plate !== undefined, `받침 ${r.no} ${dim === 'aMm' ? 'A' : 'B'} (mm)`, (v) => {
|
||||
const next = { ...(settings.plates ?? {}) };
|
||||
const cur = next[key] ?? { aMm: r.A, bMm: r.B };
|
||||
if (v === undefined) delete next[key]; else next[key] = { ...cur, [dim]: v };
|
||||
setSole(pier.input.name, { ...settings, plates: next });
|
||||
}));
|
||||
}
|
||||
const cells = [String(r.L), String(r.W), `${r.T}${r.tTyped ? ' (수정됨)' : ''}`, String(r.T1), String(r.T2),
|
||||
`${r.checkLhs} ≈ ${r.checkRhs.toFixed(1)} ${r.ok ? '일치' : '불일치 (W08)'}`, r.warnings.length ? r.warnings.join(' · ') : '—'];
|
||||
for (const c of cells) tr.appendChild(el('td', c));
|
||||
body.appendChild(tr);
|
||||
}
|
||||
@@ -5137,47 +5171,79 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
}
|
||||
|
||||
/** Sets one pier's solplate rule (C안, 2026-11-03) and rebuilds that pier. */
|
||||
function setSolplateRule(name: string, rule: SolplateRule): void {
|
||||
/** Sets one pier's solplate settings (2026-11-06) and rebuilds that pier. */
|
||||
function setSole(name: string, sole: SoleSettings): void {
|
||||
const p = state.piers.find((q) => q.input.name === name);
|
||||
if (!p) return;
|
||||
p.draft = { ...p.draft, solplate: rule };
|
||||
const issues = soleSettingsIssues(sole);
|
||||
if (issues.length) { setError(issues.join(' · ')); return; }
|
||||
p.draft = { ...p.draft, sole };
|
||||
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||
setupScene();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** 솔플레이트 규칙 (C안, 2026-11-03): per pier. The plan L × W and the slope S are inputs; 받침높이 H is fixed (211 mm, not on the UI). T and h3 follow. */
|
||||
/**
|
||||
* 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유 (L − A = W − B), 열마다 T (비우면 자동), 교각 종단경사
|
||||
* (짝 받침이 없을 때). 거더 하면을 고정하므로 T가 T_ref와 다르면 블럭 상면 = z − (T − T_ref).
|
||||
*/
|
||||
function renderSolplateRule(wrap: HTMLDivElement, pier: Pier): void {
|
||||
const rule = pier.draft.solplate ?? SOLPLATE_RULE_DEFAULT;
|
||||
wrap.appendChild(el('p', `솔플레이트 규칙 (이 교각 · 기본 1000 × 1100 · −0.5 %) · 받침높이 H ${rule.heightMm} mm 고정 (솔플레이트 포함)`, 'lbl'));
|
||||
const fields: [keyof SolplateRule, string][] = [
|
||||
['lengthMm', '솔플레이트 교직 L (mm)'],
|
||||
['widthMm', '솔플레이트 교축 W (mm)'],
|
||||
['slopePct', '종단 기울기 S (%)'],
|
||||
];
|
||||
const sole = pier.draft.sole ?? {};
|
||||
wrap.appendChild(el('p', `솔플레이트 (결정 2026-11-06) · T = 열마다 max(${sole.tMinMm ?? SOLE_DEFAULTS.tMinMm}, ⌈${sole.tMinMm ?? SOLE_DEFAULTS.tMinMm} + L·|S|/2⌉) · 거더 하면 고정`, 'lbl'));
|
||||
const grid = el('div', undefined, 'edit-row');
|
||||
for (const [key, label] of fields) {
|
||||
const id = `sol-${key}`;
|
||||
const lab = el('label', label);
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'number';
|
||||
input.step = 'any';
|
||||
input.value = String(rule[key]);
|
||||
if (rule[key] !== SOLPLATE_RULE_DEFAULT[key]) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => setSolplateRule(pier.input.name, { ...rule, [key]: Number(input.value) }));
|
||||
grid.append(lab, input, el('span', `기본 ${SOLPLATE_RULE_DEFAULT[key]}`, 'muted small'));
|
||||
const field = (id: string, label: string, value: number | undefined, base: string, apply: (v: number | undefined) => void) => {
|
||||
const lab = el('label', label); lab.htmlFor = id;
|
||||
const input = el('input'); input.id = id; input.type = 'number'; input.step = 'any';
|
||||
input.value = value === undefined ? '' : String(value);
|
||||
input.placeholder = base;
|
||||
if (value !== undefined) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => apply(input.value.trim() === '' ? undefined : Number(input.value)));
|
||||
grid.append(lab, input, el('span', `기본 ${base}`, 'muted small'));
|
||||
};
|
||||
const set = (patch: Partial<SoleSettings>) => setSole(pier.input.name, { ...sole, ...patch });
|
||||
field('sole-h3', '받침 높이 h3 (mm)', sole.h3Mm, String(SOLE_DEFAULTS.h3Mm), (v) => set({ h3Mm: v }));
|
||||
field('sole-tref', 'T_ref · 엑셀 z 산정 때 두께 (mm)', sole.tRefMm, String(SOLE_DEFAULTS.tRefMm), (v) => set({ tRefMm: v }));
|
||||
field('sole-tmin', '솔플 최소두께 T_min (mm)', sole.tMinMm, String(SOLE_DEFAULTS.tMinMm), (v) => set({ tMinMm: v }));
|
||||
field('sole-margin', '솔플 여유 L − A = W − B (mm)', sole.marginMm, String(SOLE_DEFAULTS.marginMm), (v) => set({ marginMm: v }));
|
||||
const rows = [...new Set(pier.input.bearings.map((b) => b.row))];
|
||||
for (const row of rows) {
|
||||
const auto = pier.model?.soles.find((s) => s.row === row);
|
||||
field(`sole-t-${row}`, `솔플레이트 T · ${row} (mm, 비우면 자동)`, sole.tByRow?.[row], auto && !auto.tTyped ? `자동 ${auto.T}` : '자동', (v) => {
|
||||
const next = { ...(sole.tByRow ?? {}) };
|
||||
if (v === undefined) delete next[row]; else next[row] = v;
|
||||
set({ tByRow: next });
|
||||
});
|
||||
}
|
||||
const lsId = 'sole-longitudinal';
|
||||
const lsLab = el('label', '교각 종단경사 (소수, 짝 받침이 없을 때)'); lsLab.htmlFor = lsId;
|
||||
const ls = el('input'); ls.id = lsId; ls.type = 'number'; ls.step = 'any';
|
||||
ls.value = pier.draft.longitudinalSlope === undefined ? '' : String(pier.draft.longitudinalSlope);
|
||||
ls.placeholder = '입력 없음';
|
||||
ls.addEventListener('change', () => {
|
||||
const p = state.piers.find((q) => q.input.name === pier.input.name);
|
||||
if (!p) return;
|
||||
const v = ls.value.trim() === '' ? undefined : Number(ls.value);
|
||||
if (v !== undefined && !Number.isFinite(v)) { setError('종단경사는 숫자여야 합니다.'); return; }
|
||||
p.draft = { ...p.draft, longitudinalSlope: v };
|
||||
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||
setupScene();
|
||||
renderAll();
|
||||
});
|
||||
grid.append(lsLab, ls, el('span', '(−) = 교량진행방향으로 낮아짐 · Pier 시트 새 열(Q-SH2)', 'muted small'));
|
||||
wrap.appendChild(grid);
|
||||
const issues = solplateRuleIssues(rule);
|
||||
if (issues.length > 0) {
|
||||
wrap.appendChild(el('p', issues.join(' '), 'warn-text'));
|
||||
return;
|
||||
}
|
||||
const plan = solplatePlan(rule);
|
||||
wrap.appendChild(el('p', `중심 두께 T = ${plan.T} mm (T2 ≥ 22 mm) · 받침 본체 h3 = H − T = ${plan.h3} mm · T1 = ${plan.T1.toFixed(1)} mm · T2 = ${plan.T2.toFixed(1)} mm`, 'muted small'));
|
||||
const m = pier.model;
|
||||
if (!m) return;
|
||||
const unpaired = m.soles.filter((s) => s.warnings.includes('WS03')).map((s) => s.no);
|
||||
const notes = [
|
||||
`받침 A × B 기본 ${SOLE_DEFAULTS.aMm} × ${SOLE_DEFAULTS.bMm} (P1 전열 값, 가정) · 표에서 받침별 수정`,
|
||||
unpaired.length ? `WS03 짝 없음: ${unpaired.join(', ')}번 → ${m.soles.find((s) => s.no === unpaired[0])?.slopeSource === '입력' ? '종단경사 입력' : '다른 열 값 또는 0 %'} 사용` : '모든 받침이 짝 받침으로 S 계산',
|
||||
...['WS01', 'WS02', 'WS04'].flatMap((w) => {
|
||||
const hit = m.soles.filter((s) => s.warnings.includes(w as 'WS01')).map((s) => s.no);
|
||||
return hit.length ? [`${w}: ${hit.join(', ')}번`] : [];
|
||||
}),
|
||||
...rows.map((row) => { const s = m.soles.find((x) => x.row === row)!; return `${row} 블럭 상면 보정 ${s.dropMm === 0 ? '0' : `${-s.dropMm > 0 ? '+' : ''}${-s.dropMm} mm`}`; }),
|
||||
];
|
||||
wrap.appendChild(el('p', `검사 · ${notes.join(' · ')}`, 'muted small'));
|
||||
}
|
||||
|
||||
/** ② 코핑 상세 (2단계, 읽기 전용): d_IP2 and the 계단 구간 표 from the model (승인 2026-10-23). */
|
||||
@@ -5512,6 +5578,7 @@ function openProjectText(text: string, saved: boolean): void {
|
||||
groundModel = parsed.groundModel ?? null;
|
||||
groundMapMode = groundModel?.mapLayers?.length ? 'satellite' : 'none';
|
||||
setGroundMapMode(groundMapMode);
|
||||
state.bridgePairs = bridgePairs(parsed.piers.map((p) => p.draft ?? p.input), state.abutments);
|
||||
state.piers = parsed.piers.map((p) => ({
|
||||
...buildEntry(p.input, false),
|
||||
defaults: p.defaults ?? p.input,
|
||||
@@ -5752,6 +5819,8 @@ function loadBytes(bytes: Uint8Array, fileName: string): void {
|
||||
const wb = XLSX.read(bytes, { type: 'array' });
|
||||
const { piers, sheets } = parseWorkbook(wb);
|
||||
const abut = parseAbutments(wb, piers.map((pier) => pier.name));
|
||||
// Solplate S needs the girder pairs of the whole file (2026-11-06); applyImport recomputes them for the chosen ones.
|
||||
state.bridgePairs = bridgePairs(piers, abut.abutments);
|
||||
const entries = piers.map((input) => buildEntry(input, false));
|
||||
// A sheet is selectable only if every pier in it builds. Show the reason next to the sheet.
|
||||
for (const sheet of sheets) {
|
||||
@@ -5794,6 +5863,9 @@ function applyImport(): void {
|
||||
state.piers = p.entries.filter((e) => e.checked && selectable(p, e));
|
||||
state.abutments = p.abutments.filter((rec) => p.selectedAbutments.has(rec.name) && selectableAbut(p, rec));
|
||||
state.abutDefaults = JSON.parse(JSON.stringify(state.abutments)) as AbutRecord[];
|
||||
// The chosen structures may differ from the whole file: rebuild with their girder pairs (솔플레이트 S, 2026-11-06).
|
||||
state.bridgePairs = bridgePairs(state.piers.map((e) => e.input), state.abutments);
|
||||
state.piers = state.piers.map((e) => ({ ...buildEntry(e.input, e.checked), defaults: e.defaults, draft: e.draft }));
|
||||
state.active = state.piers[0]?.input.name ?? null;
|
||||
state.filter = 'all';
|
||||
state.pending = null;
|
||||
|
||||
@@ -69,3 +69,11 @@ describe('솔플레이트 열 두께 (Q-SH3·8·9, L 800)', () => {
|
||||
expect(row.ends[1]).toEqual({ T1: 38, T2: 30 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('교량 전체 짝 받침 (bridgePairs, 구조물 순서 A1 · P1 … · A2)', () => {
|
||||
it('builds the same A1 ↔ P1 pairs from the loaded records', async () => {
|
||||
const { bridgePairs } = await import('../src/core/bridge-pairs');
|
||||
const pairs = bridgePairs([p1], [a1]);
|
||||
expect(pairs.map((p) => `${p.from.structure}${p.from.no}-${p.to.structure}${p.to.no}`)).toEqual(['A11-P11', 'A12-P12', 'A13-P13', 'A14-P14', 'A15-P15']);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { bearingPlanSvg } from '../src/view/bearing-plan-svg';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
const separate = buildPier(loadP1());
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
const separate = buildPier(loadP1(), P1CTX);
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' }, P1CTX);
|
||||
|
||||
describe('교량받침 상세 평면 (승인 2026-10-26)', () => {
|
||||
it('shows C4 and C5 from the bearing to the block edges along the 교축 (600 each)', () => {
|
||||
@@ -18,8 +21,8 @@ describe('교량받침 상세 평면 (승인 2026-10-26)', () => {
|
||||
expect(bearingPlanSvg(integrated, 0)).toContain('width="2500" height="1200"');
|
||||
});
|
||||
|
||||
it('draws the solplate of each bearing from the pier plan (L 1000 across, W 1100 along)', () => {
|
||||
expect((bearingPlanSvg(separate, 0).match(/width="1000" height="1100"/g) ?? []).length).toBe(2);
|
||||
it('draws the solplate of each bearing from its plan (L = A + 100 = 800, W = B + 100 = 740, 2026-11-06)', () => {
|
||||
expect((bearingPlanSvg(separate, 0).match(/width="800" height="740"/g) ?? []).length).toBe(2);
|
||||
});
|
||||
|
||||
it('an unknown pair index gives an empty drawing', () => {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { bearingSectionSvg } from '../src/view/bearing-section-svg';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
const model = buildPier(loadP1());
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
const model = buildPier(loadP1(), P1CTX);
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' }, P1CTX);
|
||||
|
||||
describe('교량받침 상세 단면 (승인 2026-10-26, C안 2026-11-03)', () => {
|
||||
it('labels EL.X at the mortar top and the block height h1 for each bearing', () => {
|
||||
|
||||
+17
-10
@@ -1,10 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { elevationRows, pairCopingSpans, solplateRows } from '../src/core/bearing-table';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
const model = buildPier(loadP1());
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
const model = buildPier(loadP1(), P1CTX);
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' }, P1CTX);
|
||||
|
||||
describe('교량받침 ELEVATION TABLE (모델값, 2026-11-03)', () => {
|
||||
const rows = elevationRows(model);
|
||||
@@ -15,9 +18,9 @@ describe('교량받침 ELEVATION TABLE (모델값, 2026-11-03)', () => {
|
||||
expect(rows[5].elx).toBeCloseTo(43.611, 3);
|
||||
});
|
||||
|
||||
it('front block 100 mm, back block 150 mm; mortar 50; bearing 186; solplate 25; E = sum', () => {
|
||||
expect(rows[0]).toMatchObject({ h1: 100, h2: 50, h3: 186, h4: 25, E: 361 });
|
||||
expect(rows[5]).toMatchObject({ h1: 150, h2: 50, h3: 186, h4: 25, E: 411 });
|
||||
it('front block 100 mm, back block 150 mm; mortar 50; bearing 181; solplate 30 (T = T_ref → no drop); E = sum', () => {
|
||||
expect(rows[0]).toMatchObject({ h1: 100, h2: 50, h3: 181, h4: 30, E: 361 });
|
||||
expect(rows[5]).toMatchObject({ h1: 150, h2: 50, h3: 181, h4: 30, E: 411 });
|
||||
});
|
||||
|
||||
it('C1 is 600 for a 분리 bearing; C6 is the 1200 block depth; C4 and C5 are 600 each', () => {
|
||||
@@ -47,12 +50,16 @@ describe('교량받침 ELEVATION TABLE (모델값, 2026-11-03)', () => {
|
||||
});
|
||||
|
||||
describe('솔플레이트 TABLE (모델값, 2026-11-03)', () => {
|
||||
it('10 rows with S −0.5, L 1000, W 1100, T 25, T1 27.5, T2 22.5; the check holds', () => {
|
||||
it('10 rows: S from the A1 pair (−1.908 %), L 800, W 740, T 30, T1 38, T2 22; W08 holds within 1 mm (2026-11-06)', () => {
|
||||
const rows = solplateRows(model);
|
||||
expect(rows.length).toBe(10);
|
||||
expect(rows[0]).toMatchObject({ S: -0.5, L: 1000, W: 1100, T: 25, T1: 27.5, T2: 22.5, ok: true });
|
||||
expect(rows[0].checkLhs).toBeCloseTo(5, 9);
|
||||
expect(rows[0].checkRhs).toBeCloseTo(5, 9);
|
||||
expect(rows[0]).toMatchObject({ L: 800, W: 740, T: 30, T1: 38, T2: 22, ok: true, pair: 'A1-①', source: '계산' });
|
||||
expect(rows[0].S).toBeCloseTo(-1.908, 3);
|
||||
expect(rows[0].checkLhs).toBe(16);
|
||||
expect(rows[0].checkRhs).toBeCloseTo(15.265, 2);
|
||||
// 후열 ⑥: no pair (P2 not loaded) → the front twin ① value, 가정 + WS03.
|
||||
expect(rows[5]).toMatchObject({ source: '가정', pair: undefined, T: 30 });
|
||||
expect(rows[5].warnings).toContain('WS03');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
const m = buildPier(loadP1());
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
const front = m.blocks.filter((b) => b.row === '전열');
|
||||
const back = m.blocks.filter((b) => b.row === '후열');
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { fileURLToPath } from 'node:url';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseWorkbook } from '../src/core/parseWorkbook';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
import { parseAbutments } from '../src/core/abut/parse';
|
||||
import { type BearingPair, girderPairs } from '../src/core/bearing-pair';
|
||||
import { buildPier, type PierModel } from '../src/core/geometry';
|
||||
|
||||
/** Spec workbook (RAG/Pier ui_xxx.xlsx, sheet intent.PR, all tables on one sheet). Used by single-sheet tests. */
|
||||
const SPEC_PATH = fileURLToPath(new URL('../public/samples/P1.xlsx', import.meta.url));
|
||||
@@ -24,3 +27,22 @@ export function loadP1(): PierInput {
|
||||
if (!p1) throw new Error('P1 not found');
|
||||
return p1;
|
||||
}
|
||||
|
||||
/**
|
||||
* P1 with its bridge context (2026-11-06): the girder pairs A1 ①–⑤ ↔ P1 ①–⑤ from the same input workbook, so the
|
||||
* solplate slope S is computed (−1.908 … −1.902 %, T 30) as in the app where A1 is loaded with P1.
|
||||
*/
|
||||
export function p1Pairs(): BearingPair[] {
|
||||
const a1 = parseAbutments(loadInputWorkbook()).abutments.find((a) => a.name === 'A1');
|
||||
if (!a1) throw new Error('A1 not found');
|
||||
const p1 = loadP1();
|
||||
return girderPairs([
|
||||
{ name: 'A1', bearings: a1.bearings.map((b) => ({ no: b.no, x: b.pos.e, y: b.pos.n, z: b.z, azimuthDeg: b.azimuthDeg! })) },
|
||||
{ name: p1.name, bearings: p1.bearings.map((b) => ({ no: b.no, x: b.x, y: b.y, z: b.z, azimuthDeg: b.azimuthDeg })) },
|
||||
]);
|
||||
}
|
||||
|
||||
/** buildPier for P1 (with changes) in its bridge context. */
|
||||
export function buildP1(changes: Partial<PierInput> = {}): PierModel {
|
||||
return buildPier({ ...loadP1(), ...changes }, { pairs: p1Pairs() });
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
/**
|
||||
* Regression against spec.md §12 (P1, 운곡교, T형, 말뚝기초).
|
||||
* Coordinates and EL: ±1 mm (0.001 m). Angles: ±0.01°.
|
||||
*/
|
||||
const TOL = 0.001;
|
||||
const model = buildPier(loadP1());
|
||||
const model = buildPier(loadP1(), P1CTX);
|
||||
|
||||
describe('P1 regression — spec.md §12', () => {
|
||||
it('axis length and azimuth', () => {
|
||||
|
||||
@@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { barsAcross, bearingBlockRebar, blockBars, totals } from '../src/core/rebar/bearing-block';
|
||||
import { rebarSummary } from '../src/core/rebar/summary';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
|
||||
it('a 1200 block takes 8 bars at 125 mm: 7 gaps = 875, ends 162.5 each', () => {
|
||||
@@ -18,7 +21,7 @@ describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
|
||||
});
|
||||
|
||||
it('coping embedment from the model block height (block top − coping top: front 100, back 150): S2 650 / 600, S1 669.1 / 619.1', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
const rebar = bearingBlockRebar(m);
|
||||
const emb = (row: '전열' | '후열', name: 'S1' | 'S2') => rebar.find((b) => b.row === row)!.bars.find((r) => r.name === name)!.embedment;
|
||||
expect(emb('전열', 'S2')).toBeCloseTo(650, 6);
|
||||
@@ -28,7 +31,7 @@ describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
|
||||
});
|
||||
|
||||
it('P1: 10 blocks, 80 bars each of S1 and S2 (spec table: 8 本 per block)', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
const rebar = bearingBlockRebar(m);
|
||||
expect(rebar).toHaveLength(10);
|
||||
expect(totals(rebar)).toEqual({ S1: 80, S2: 80 });
|
||||
@@ -52,7 +55,7 @@ describe('받침블럭 도면 재현 (spec §6.3)', () => {
|
||||
|
||||
describe('받침블럭 묻힘 검토 (spec §9 RB-S04)', () => {
|
||||
it('P1: embedments 619–669 mm are shorter than the H19 development length 882, so RB-S04 is raised and the rows show the range', () => {
|
||||
const { rows, warnings } = rebarSummary(buildPier(loadP1()));
|
||||
const { rows, warnings } = rebarSummary(buildPier(loadP1(), P1CTX));
|
||||
expect(warnings.some((w) => w.code === 'RB-S04')).toBe(true);
|
||||
expect(rows.find((r) => r.name === 'S1')!.note).toContain('묻힘 619–669');
|
||||
expect(rows.find((r) => r.name === 'S2')!.note).toContain('묻힘 600–650');
|
||||
|
||||
+7
-4
@@ -2,9 +2,12 @@ import * as THREE from 'three';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { buildPierScene, frameMatrix } from '../src/view/scene';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
const model = buildPier(loadP1());
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
const model = buildPier(loadP1(), P1CTX);
|
||||
const M = frameMatrix(model);
|
||||
|
||||
/** Scene coordinates of a national-grid point at EL 0 (x = E − pivot E, z = −(N − pivot N)). */
|
||||
@@ -29,11 +32,11 @@ describe('3D frame (view/scene.ts)', () => {
|
||||
expect(mapped.distanceTo(expected)).toBeLessThan(1e-6);
|
||||
});
|
||||
|
||||
it('bounding box spans pile tip (2.208 m) to the highest solplate top (back bearing 5: pad top 43.898 m + h3 0.186 m + T1 0.0275 m)', () => {
|
||||
it('bounding box spans pile tip (2.208 m) to the highest solplate top (back bearing 5: pad top 43.898 m + h3 0.181 m + T1 0.038 m)', () => {
|
||||
const scene = buildPierScene(model);
|
||||
const box = new THREE.Box3().setFromObject(scene.root);
|
||||
expect(box.min.y).toBeCloseTo(2.208, 2);
|
||||
expect(box.max.y).toBeCloseTo(43.898 + 0.186 + 0.0275, 3);
|
||||
expect(box.max.y).toBeCloseTo(43.898 + 0.181 + 0.038, 3);
|
||||
});
|
||||
|
||||
it('builds the expected part counts for P1', () => {
|
||||
|
||||
+10
-7
@@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { validatePier } from '../src/core/validate';
|
||||
import type { Issue, PierInput } from '../src/core/types';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
|
||||
// 교좌면 EL of the P1 sheet (받침 1–10), 2026-11-03.
|
||||
const SEAT = [43.411, 43.483, 43.555, 43.626, 43.698, 43.411, 43.483, 43.555, 43.626, 43.698];
|
||||
@@ -13,8 +16,8 @@ function withSeat(input: PierInput, seats: number[]): PierInput {
|
||||
|
||||
describe('교좌면 EL 입력 (2026-11-03)', () => {
|
||||
it('the P1 교좌면 EL values reproduce the 두부보 상면 slope: the model does not change', () => {
|
||||
const base = buildPier(loadP1());
|
||||
const seated = buildPier(withSeat(loadP1(), SEAT));
|
||||
const base = buildPier(loadP1(), P1CTX);
|
||||
const seated = buildPier(withSeat(loadP1(), SEAT), P1CTX);
|
||||
for (const b of base.blocks) {
|
||||
const s = seated.blocks.find((x) => x.no === b.no)!;
|
||||
expect(s.bottom).toBeCloseTo(b.bottom, 6);
|
||||
@@ -23,20 +26,20 @@ describe('교좌면 EL 입력 (2026-11-03)', () => {
|
||||
});
|
||||
|
||||
it('an input 교좌면 EL sets the block bottom and so h1 = (EL.X − 50 mm) − 교좌면 EL', () => {
|
||||
const m = buildPier(withSeat(loadP1(), [43.4, ...SEAT.slice(1)]));
|
||||
const m = buildPier(withSeat(loadP1(), [43.4, ...SEAT.slice(1)]), P1CTX);
|
||||
const b1 = m.blocks.find((x) => x.no === 1)!;
|
||||
expect(b1.bottom).toBeCloseTo(43.4, 6);
|
||||
expect((b1.top - b1.bottom) * 1000).toBeCloseTo(111, 6);
|
||||
});
|
||||
|
||||
it('without 교좌면 EL the seatCalc is the slope value (the check reference)', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
expect(m.blocks.every((b) => Math.abs(b.seatCalc - b.bottom) < 1e-9)).toBe(true);
|
||||
});
|
||||
|
||||
it('warns (W10) when the input 교좌면 EL differs from the slope by more than 5 mm, and keeps the input', () => {
|
||||
const input = withSeat(loadP1(), [43.5, ...SEAT.slice(1)]);
|
||||
const m = buildPier(input);
|
||||
const m = buildPier(input, P1CTX);
|
||||
const issues: Issue[] = [];
|
||||
validatePier(input, m, issues);
|
||||
const w = issues.filter((i) => i.code === 'W10');
|
||||
@@ -47,7 +50,7 @@ describe('교좌면 EL 입력 (2026-11-03)', () => {
|
||||
|
||||
it('no W10 for the P1 values', () => {
|
||||
const input = withSeat(loadP1(), SEAT);
|
||||
const m = buildPier(input);
|
||||
const m = buildPier(input, P1CTX);
|
||||
const issues: Issue[] = [];
|
||||
validatePier(input, m, issues);
|
||||
expect(issues.filter((i) => i.code === 'W10')).toEqual([]);
|
||||
|
||||
@@ -3,22 +3,64 @@ import * as THREE from 'three';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { solplateGeometry } from '../src/core/solplate';
|
||||
import { buildPierScene } from '../src/view/scene';
|
||||
import { loadP1 } from './helpers';
|
||||
import { loadP1, p1Pairs } from './helpers';
|
||||
|
||||
describe('솔플레이트 모델과 3D (C안, 2026-11-03)', () => {
|
||||
it('every pier has a solplate plan from its rule; the default gives T 25 and h3 186', () => {
|
||||
const m = buildPier(loadP1());
|
||||
expect(m.blocks.every((b) => b.plan.T === 25 && b.plan.h3 === 186)).toBe(true);
|
||||
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
|
||||
const P1CTX = { pairs: p1Pairs() };
|
||||
const FRONT = { 1: 3, 2: 3, 3: 3, 4: 3, 5: 3 };
|
||||
|
||||
describe('솔플레이트 모델과 3D (결정 2026-11-06, Q-SH1–9)', () => {
|
||||
it('every bearing: h3 181, L 800 × W 740 (A 700 × B 640 + 100), T 30, T1 38 / T2 22', () => {
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
expect(m.blocks.every((b) => b.plan.h3 === 181 && b.plan.L === 800 && b.plan.W === 740 && b.plan.T === 30 && b.plan.T1 === 38 && b.plan.T2 === 22)).toBe(true);
|
||||
});
|
||||
|
||||
it('a pier rule changes its own plan: 받침높이 211 and slope −1 % give T 27 and h3 184', () => {
|
||||
const m = buildPier({ ...loadP1(), solplate: { heightMm: 211, lengthMm: 1000, widthMm: 1100, slopePct: -1 } });
|
||||
expect(m.blocks[0].plan.T).toBe(27);
|
||||
expect(m.blocks[0].plan.h3).toBe(184);
|
||||
it('S 3 % typed on the front row → T 34, T1 46 / T2 22, and the front blocks drop 4 mm (girder underside kept)', () => {
|
||||
const base = buildPier(loadP1(), P1CTX);
|
||||
const m = buildPier({ ...loadP1(), sole: { slopePct: FRONT } }, P1CTX);
|
||||
const b1 = m.blocks.find((b) => b.no === 1)!, b0 = base.blocks.find((b) => b.no === 1)!;
|
||||
expect(b1.plan).toMatchObject({ T: 34, T1: 46, T2: 22 });
|
||||
expect(b1.top).toBeCloseTo(b0.top - 0.004, 9);
|
||||
expect(b1.padTop + (b1.plan.h3 + b1.plan.T) / 1000).toBeCloseTo(b0.padTop + (b0.plan.h3 + b0.plan.T) / 1000, 9);
|
||||
expect(m.soles.find((s) => s.no === 1)!.slopeSource).toBe('수정됨');
|
||||
// The back row keeps its own T (30): one T per row (Q-SH9).
|
||||
expect(m.blocks.find((b) => b.no === 6)!.plan.T).toBe(30);
|
||||
});
|
||||
|
||||
it('WS02: T typed 30 at S 3 % leaves T2 18 < 22', () => {
|
||||
const m = buildPier({ ...loadP1(), sole: { slopePct: FRONT, tByRow: { 전열: 30 } } }, P1CTX);
|
||||
const s = m.soles.find((x) => x.no === 1)!;
|
||||
expect(s).toMatchObject({ T: 30, tTyped: true, T2: 18 });
|
||||
expect(s.warnings).toContain('WS02');
|
||||
});
|
||||
|
||||
it('without a pair or 종단경사 the slope is 0 (WS03): T 22, the block rises 8 mm', () => {
|
||||
const alone = buildPier(loadP1());
|
||||
expect(alone.soles.every((s) => s.slopePct === 0 && s.warnings.includes('WS03') && s.T === 22)).toBe(true);
|
||||
const withSlope = buildPier({ ...loadP1(), longitudinalSlope: -0.0191 });
|
||||
expect(withSlope.soles.every((s) => s.slopeSource === '입력' && Math.abs(s.slopePct + 1.91) < 1e-9 && s.T === 30)).toBe(true);
|
||||
});
|
||||
|
||||
it('T1 sits on the 오르막 쪽: the thick end of P1 ① points towards A1 (the higher girder end)', () => {
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
const b = m.blocks.find((x) => x.no === 1)!;
|
||||
const g = solplateGeometry(b, b.plan);
|
||||
// Top corners: index (ai·2 + ci)·2 + 1; the thick end has the larger top.
|
||||
const tops = [0, 1, 2, 3].map((q) => g.corners[q * 2 + 1]);
|
||||
const thick = tops.filter((p) => p[1] - g.bottom > (g.T1 + g.T2) / 2), thin = tops.filter((p) => p[1] - g.bottom < (g.T1 + g.T2) / 2);
|
||||
const mid = (ps: number[][]) => [0, 2].map((i) => ps.reduce((s, p) => s + p[i], 0) / ps.length);
|
||||
const [ds, dn] = [mid(thick)[0] - mid(thin)[0], mid(thick)[1] - mid(thin)[1]];
|
||||
const toThick = { e: ds * m.u.e + dn * m.nrm.e, n: ds * m.u.n + dn * m.nrm.n };
|
||||
const p1 = loadP1().bearings[0];
|
||||
const pair = P1CTX.pairs.find((p) => p.to.no === 1)!;
|
||||
// A1 lies against the azimuth direction from P1 (pairs look ahead from A1 to P1).
|
||||
const az = (p1.azimuthDeg * Math.PI) / 180;
|
||||
expect(pair.uphill).toBe('A1');
|
||||
expect(toThick.e * -Math.sin(az) + toThick.n * -Math.cos(az)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('the 3D scene has one solplate wedge per bearing, and its highest point is the thick end of the tallest one', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const m = buildPier(loadP1(), P1CTX);
|
||||
const scene = buildPierScene(m);
|
||||
const wedges = scene.parts.solplates.children;
|
||||
expect(wedges.length).toBe(10);
|
||||
@@ -27,13 +69,11 @@ describe('솔플레이트 모델과 3D (C안, 2026-11-03)', () => {
|
||||
expect(box.max.y).toBeCloseTo(top, 3);
|
||||
});
|
||||
|
||||
it('the 교량받침 본체 is one box per bearing: the plan less 100 mm on each side, height h3 (186 mm)', () => {
|
||||
const scene = buildPierScene(buildPier(loadP1()));
|
||||
it('the 교량받침 본체 is one box per bearing, height h3 (181 mm)', () => {
|
||||
const scene = buildPierScene(buildPier(loadP1(), P1CTX));
|
||||
const bodies = scene.parts.bearings.children;
|
||||
expect(bodies.length).toBe(10);
|
||||
const one = new THREE.Box3().setFromObject(bodies[0]);
|
||||
expect(one.max.y - one.min.y).toBeCloseTo(0.186, 3);
|
||||
// Body: L − 200 = 800 across the 교각축 직각, W − 200 = 900 along the 교축; the row skew turns it a few degrees.
|
||||
expect(Math.max(one.max.x - one.min.x, one.max.z - one.min.z)).toBeGreaterThan(0.85);
|
||||
expect(one.max.y - one.min.y).toBeCloseTo(0.181, 3);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user