feat(bearing): 솔플레이트 규칙을 교각별 입력으로 바꾸고 도면 불러오기 제거 (C안)

- 받침 기준 카드, 도면 불러오기(xlsx·csv·dxf), 도면 참조 표·오버레이 제거
- 교각마다 솔플레이트 규칙 입력: 받침높이 H(솔플레이트 포함) 211, 교직 L 1000,
  교축 W 1100, 종단 기울기 S −0.5 % (기본값, 가정 표시)
- 중심 두께 T = ⌈22 + L·|S|/200⌉ 로 계산해 얇은 쪽 T2가 22 mm 이상이 되게 함
  (기본값 T 25, T2 22.5, h3 = H − T = 186)
- geometry·scene·SVG는 교각 규칙에서 만든 같은 솔플레이트 계획을 모든 받침에 사용

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 11:07:51 +09:00
1 parent f25e36c050
commit 36b26209df
23 files changed
+257 -1081

No files matched your search

+4 -20
View File
@@ -1,19 +1,12 @@
import type { PierModel } from '../core/geometry';
import { bearingEdgeDistances, pairEccentricities } from '../core/bearing-layout';
/** Drawing values (A, B of the solplate, mm) of the front and back bearings, to overlay on the model (승인 2026-10-26). */
export interface BearingPlanOverlay {
front: { A: number; B: number };
back: { A: number; B: number };
}
/** Number of pairs the 상세 보기 chips can show (①–⑩ in the mockup). */
export const PAIR_CHIP_LABELS = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
/**
* 교량받침 상세 · 평면 for one pair (승인 2026-10-26): the bearings keep their actual 교각축 positions (the offset
* between the two bearings is shown as it is), and the blocks and solplates follow them. The model's solplates are solid, the drawing values
* dashed. 일체 draws one 2500 × 1200 outline; 분리 draws two 1200 × 1200 outlines. Dimensions: C1 (bearing to block edge),
* between the two bearings is shown as it is), and the blocks and solplates follow them. 일체 draws one 2500 × 1200 outline; 분리 draws two 1200 × 1200 outlines. Dimensions: C1 (bearing to block edge),
* C2 (between the bearings), C3 (block span). C4 and C5 are the outer margins along the 교축.
*/
/**
@@ -24,7 +17,7 @@ function rect(cx: number, cy: number, w: number, h: number, skewDeg: number, att
return `<rect x="${cx - w / 2}" y="${cy - h / 2}" width="${w}" height="${h}" transform="rotate(${-skewDeg} ${cx} ${cy})" ${attrs}></rect>`;
}
export function bearingPlanSvg(m: PierModel, pairIndex: number, overlay?: BearingPlanOverlay): string {
export function bearingPlanSvg(m: PierModel, pairIndex: number): string {
const pair = pairEccentricities(m.blocks)[pairIndex];
if (!pair) return '';
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
@@ -55,18 +48,9 @@ export function bearingPlanSvg(m: PierModel, pairIndex: number, overlay?: Bearin
}
}
// Solplates: the model's plan (A across the 교축, B along the 교각축), centred on each bearing.
// Solplates: the pier's plan (L across the 교축, W along the 교각축), centred on each bearing.
for (const b of [f, k]) {
if (b.plan) {
const { A, B } = b.plan;
parts.push(rect(X(b.n), Y(b.s), A, B, b.rowSkewDeg, `fill="#EEF3F7" stroke="#2F6DB5" stroke-width="10"`));
}
}
// Drawing values overlaid (dashed) on the same bearings (#1 겹쳐 보이게).
if (overlay) {
for (const [b, v] of [[f, overlay.front], [k, overlay.back]] as const) {
parts.push(rect(X(b.n), Y(b.s), v.A, v.B, b.rowSkewDeg, `fill="none" stroke="#B4531F" stroke-width="10" stroke-dasharray="40 25"`));
}
parts.push(rect(X(b.n), Y(b.s), b.plan.L, b.plan.W, b.rowSkewDeg, `fill="#EEF3F7" stroke="#2F6DB5" stroke-width="10"`));
}
// Bearing centres and numbers.
+11 -44
View File
@@ -1,27 +1,12 @@
import type { PierModel } from '../core/geometry';
import { pairEccentricities } from '../core/bearing-layout';
/**
* Drawing values of one bearing (도면값), overlaid as dashed outlines (승인 2026-10-26).
* All lengths in mm; elx in m.
*/
export interface BearingDrawingRef {
elx?: number;
h1: number;
h2: number;
h3: number;
h4: number;
E: number;
A: number;
B: number;
}
/**
* 교량받침 상세 · 단면 (elevation) for one pair (승인 2026-10-26): the model's blocks, mortar, bearing body,
* and solplate per bearing, with the drawing heights overlaid as dashed outlines. x is the 교축 (n), y the EL (up).
* Labels: EL.X (mortar top), h1 (block), h2 (mortar), h3 (bearing), h4 (solplate), E = h1 + h2 + h3 + h4.
* and solplate per bearing. x is the 교축 (n), y the EL (up). Labels: EL.X (mortar top), h1 (block), h3 (bearing body),
* T (solplate, taper T1 at the (−) side, T2 at the (+) side).
*/
export function bearingSectionSvg(m: PierModel, pairIndex: number, refs?: { front?: BearingDrawingRef; back?: BearingDrawingRef }): string {
export function bearingSectionSvg(m: PierModel, pairIndex: number): string {
const pair = pairEccentricities(m.blocks)[pairIndex];
if (!pair) return '';
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
@@ -46,37 +31,19 @@ export function bearingSectionSvg(m: PierModel, pairIndex: number, refs?: { fron
}
// Mortar, bearing body and solplate per bearing, from the model.
for (const [b, ref] of [[f, refs?.front], [k, refs?.back]] as const) {
for (const b of [f, k]) {
const pad = 1000;
parts.push(`<rect x="${X(b.n) - pad / 2}" y="${Y(b.padTop)}" width="${pad}" height="${50}" fill="#B7C7D3" stroke="#6E8496" stroke-width="6"></rect>`);
if (b.plan) {
const { A, margin, h3, T } = b.plan;
const L = A + margin;
const bodyTop = b.padTop + h3 / 1000;
parts.push(`<rect x="${X(b.n) - (L - 200) / 2}" y="${Y(bodyTop)}" width="${L - 200}" height="${h3}" fill="#8898AA" stroke="#5B6676" stroke-width="6"></rect>`);
const drop = (L * Math.abs(b.plan.S)) / 200;
const t1 = T + drop;
const t2 = T - drop;
const x0 = X(b.n) - L / 2;
const x1 = X(b.n) + L / 2;
parts.push(`<polygon points="${x0},${Y(bodyTop)} ${x1},${Y(bodyTop)} ${x1},${Y(bodyTop + t2 / 1000)} ${x0},${Y(bodyTop + t1 / 1000)}" fill="#F3D9B8" stroke="#B4531F" stroke-width="6"></polygon>`);
}
if (ref) {
// Drawing heights from the block bottom, dashed (overlay).
const base = b.bottom;
const dBlockTop = base + (ref.h1 ?? 0) / 1000;
const dPad = dBlockTop + ref.h2 / 1000;
const dBody = dPad + ref.h3 / 1000;
const dSol = dBody + ref.h4 / 1000;
parts.push(`<rect x="${X(b.n) - 600}" y="${Y(dBlockTop)}" width="1200" height="${(dBlockTop - base) * 1000}" fill="none" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></rect>`);
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dSol)}" x2="${X(b.n) + 700}" y2="${Y(dSol)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dBody)}" x2="${X(b.n) + 700}" y2="${Y(dBody)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
}
const { L, h3, T1, T2 } = b.plan;
const bodyTop = b.padTop + h3 / 1000;
parts.push(`<rect x="${X(b.n) - (L - 200) / 2}" y="${Y(bodyTop)}" width="${L - 200}" height="${h3}" fill="#8898AA" stroke="#5B6676" stroke-width="6"></rect>`);
const x0 = X(b.n) - L / 2;
const x1 = X(b.n) + L / 2;
parts.push(`<polygon points="${x0},${Y(bodyTop)} ${x1},${Y(bodyTop)} ${x1},${Y(bodyTop + T2 / 1000)} ${x0},${Y(bodyTop + T1 / 1000)}" fill="#F3D9B8" stroke="#B4531F" stroke-width="6"></polygon>`);
// EL.X at the mortar top, bearing number.
parts.push(`<circle cx="${X(b.n)}" cy="${Y(b.padTop)}" r="30" fill="#18212B"></circle>`);
parts.push(`<text x="${X(b.n) + 60}" y="${Y(b.padTop) - 60}" font-size="90" fill="#18212B">${b.no} · EL.X ${b.padTop.toFixed(3)}</text>`);
parts.push(`<text x="${X(b.n)}" y="${Y(b.bottom) + 160}" text-anchor="middle" font-size="90" fill="#55606E">h1 ${Math.round((b.top - b.bottom) * 1000)}</text>`);
}
return `<svg viewBox="-2000 -1200 4000 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 단면: 블록, 모르타르, 받침, 솔플레이트, EL.X, 도면 높이 점선">${parts.join('')}</svg>`;
return `<svg viewBox="-2000 -1200 4000 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 단면: 블록, 모르타르, 받침, 솔플레이트, EL.X">${parts.join('')}</svg>`;
}
+5 -5
View File
@@ -152,14 +152,14 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid, groundEl: nu
pad.rotateY(turn);
pad.translate(b.s, b.padTop - 0.025, b.n);
add('pads', pad, 1, b.k);
if (b.plan) {
// 교량받침 본체 (승인 2026-10-23): the solplate plan less 100 mm on each side, from the mortar top up to the solplate.
const body = new THREE.BoxGeometry((b.plan.B + b.plan.margin) / 1000 - 0.2, b.plan.h3 / 1000, (b.plan.A + b.plan.margin) / 1000 - 0.2);
{
// 교량받침 본체: the solplate plan less 100 mm on each side, from the mortar top up to the solplate.
const body = new THREE.BoxGeometry(b.plan.W / 1000 - 0.2, b.plan.h3 / 1000, b.plan.L / 1000 - 0.2);
body.rotateY(turn);
body.translate(b.s, b.padTop + b.plan.h3 / 2000, b.n);
add('bearings', body, 1, b.k);
// Solplate: a wedge that tapers across the 교각축 직각 (T1 at the (−) side), on the bearing (승인 2026-10-23).
const sp = solplateGeometry(b, b.plan.h3, { A: b.plan.A, B: b.plan.B, margin: b.plan.margin, T: b.plan.T, slopePct: b.plan.S });
// Solplate: a wedge that tapers across the 교각축 직각 (T1 at the (−) side), on the bearing.
const sp = solplateGeometry(b, b.plan);
add('solplates', wedgeGeometry(sp.corners), 1, b.k);
}
}
+7 -29
View File
@@ -1,46 +1,24 @@
import type { BlockModel } from '../core/geometry';
/** Drawing values of one solplate (도면값), overlaid as dashed outlines. mm. */
export interface SolplateDrawingRef {
L: number;
W: number;
T1: number;
T2: number;
S: number;
}
/**
* 솔플레이트 상세 (승인 2026-10-26) for one bearing: the plan (L across the 교축, W along the 교각축) and the
* taper in side view (T1 at the (−) side, T2 at the (+) side, S % · T). Model solid, drawing dashed.
* 솔플레이트 상세 (승인 2026-10-26, C안 2026-11-03) for one bearing: the plan (L across, W along the 교각축) and the
* taper in side view (T1 at the (−) side, T2 at the (+) side, S % · T). Thickness is exaggerated for legibility.
*/
export function solplateDetailSvg(block: BlockModel, ref?: SolplateDrawingRef): string {
export function solplateDetailSvg(block: BlockModel): string {
const plan = block.plan;
if (!plan) return '';
const L = plan.A + plan.margin;
const W = plan.B + plan.margin;
const drop = (L * Math.abs(plan.S)) / 200;
const t1 = plan.T + drop;
const t2 = plan.T - drop;
const { L, W, T1: t1, T2: t2 } = plan;
const parts: string[] = [];
// Plan (left): L across, W along.
parts.push(`<rect x="${-L / 2}" y="${-W / 2}" width="${L}" height="${W}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></rect>`);
parts.push(`<text x="0" y="${-W / 2 - 40}" text-anchor="middle" font-size="60" fill="#18212B">L ${L.toFixed(0)}</text>`);
parts.push(`<text x="${L / 2 + 40}" y="0" font-size="60" fill="#18212B">W ${W.toFixed(0)}</text>`);
if (ref) {
parts.push(`<rect x="${-ref.L / 2}" y="${-ref.W / 2}" width="${ref.L}" height="${ref.W}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></rect>`);
}
// Side view (right): thickness T1 at x = −L/2 and T2 at x = +L/2, shown with the taper.
const ox = L / 2 + 500;
const sc = 4; // exaggerated thickness for legibility, labelled
parts.push(`<polygon points="${ox - L / 2},0 ${ox + L / 2},0 ${ox + L / 2},${-t2 * sc} ${ox - L / 2},${-t1 * sc}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></polygon>`);
parts.push(`<text x="${ox - L / 2}" y="${-t1 * sc - 40}" font-size="60" fill="#18212B">T1 ${t1.toFixed(0)}</text>`);
parts.push(`<text x="${ox + L / 2}" y="${-t2 * sc - 40}" text-anchor="end" font-size="60" fill="#18212B">T2 ${t2.toFixed(0)}</text>`);
parts.push(`<text x="${ox}" y="120" text-anchor="middle" font-size="60" fill="#55606E">S ${plan.S} % · T ${plan.T} (두께 과장 ×${sc})</text>`);
if (ref) {
const r1 = ref.T1;
const r2 = ref.T2;
parts.push(`<polygon points="${ox - ref.L / 2},0 ${ox + ref.L / 2},0 ${ox + ref.L / 2},${-r2 * sc} ${ox - ref.L / 2},${-r1 * sc}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></polygon>`);
}
parts.push(`<text x="${ox - L / 2}" y="${-t1 * sc - 40}" font-size="60" fill="#18212B">T1 ${t1.toFixed(1)}</text>`);
parts.push(`<text x="${ox + L / 2}" y="${-t2 * sc - 40}" text-anchor="end" font-size="60" fill="#18212B">T2 ${t2.toFixed(1)}</text>`);
parts.push(`<text x="${ox}" y="120" text-anchor="middle" font-size="60" fill="#55606E">S ${plan.slopePct} % · T ${plan.T} (두께 과장 ×${sc})</text>`);
const minX = -L / 2 - 200;
const maxX = ox + L / 2 + 200;
return `<svg viewBox="${minX} -900 ${maxX - minX} 1300" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="솔플레이트 상세: 평면 L × W, 두께 T1 · T2 (두께 과장)">${parts.join('')}</svg>`;