feat(plan): ② 교대 카드를 3D 창 아래로 옮기고 교량받침 삽도 3개 추가

- 교대 카드를 왼쪽 목록 아래에서 가운데 3D 창 아래(교각 부재 카드 자리)로 옮김. 부재 · 교대 목록에서 고른 부재의 내용을 보여 주고, 머리줄에 부재 이름과 요약을 표시
- 왼쪽 편집 영역 정리: ②의 편집 버튼과 접히는 편집 영역을 없앰. 왼쪽에는 구조물 목록과 부재 · 교대 목록만 남김
- 교량받침 카드: 블럭 배치(분리 고정, 일체 비활성), 상세 보기(받침 ①–⑤), 삽도 3개(평면 · 단면 · 솔플레이트 상세, src/view/abut-bearing-svg.ts), ELEVATION TABLE, 솔플레이트 TABLE, 공통 · 규칙 입력, 검사 줄
- 삽도 테스트 3건 추가. 전체 650건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-03 18:28:29 +09:00
1 parent 4e3950eb67
commit 4b02dde153
5 files changed
+231 -32

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
/**
* 교대 받침 상세 삽도 (2026-11-03, 시안 ② 교량받침 카드): 평면, 교축 방향 단면, 솔플레이트 상세. 순수 문자열 함수.
* 입력은 받침 표에서 이미 계산한 값뿐이다(교대 모델을 다시 읽지 않는다). 치수는 mm, EL은 m.
*/
export interface AbutBearingDetail {
no: number;
/** 블럭 평면 (정사각), 몰탈패드 평면 (정사각), mm. */
blockMm: number;
padMm: number;
/** 받침 A × B, 솔플레이트 L × W (mm). */
A: number;
B: number;
L: number;
W: number;
/** 높이 (mm): 블럭 콘크리트 h1, 패드 h2, 받침 h3, 솔플레이트 h4 = T. */
h1: number;
h2: number;
h3: number;
h4: number;
T1: number;
T2: number;
/** 블럭 평면 회전 (deg), 교대축 직각 기준. */
rotDeg: number;
/** 솔플레이트 경사 (%). */
S: number;
seatEl: number;
z: number;
}
const INK = '#18212b';
const MUTED = '#55606e';
const ORANGE = '#c46a1a';
const text = (x: number, y: number, s: string, anchor = 'middle', fill = INK) =>
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" font-size="11" fill="${fill}" font-family="JetBrains Mono, monospace">${s}</text>`;
const square = (cx: number, cy: number, w: number, h: number, style: string) =>
`<rect x="${(cx - w / 2).toFixed(1)}" y="${(cy - h / 2).toFixed(1)}" width="${w.toFixed(1)}" height="${h.toFixed(1)}" ${style}/>`;
/** 평면: 블럭 1200, 몰탈패드 1000 (점선), 솔플레이트 L × W (주황 점선), 받침 A × B. 블럭은 받침 방위각만큼 돈다. */
export function abutBearingPlanSvg(d: AbutBearingDetail): string {
const k = 0.17; // px per mm
const cx = 150;
const cy = 130;
const turn = `transform="rotate(${(-d.rotDeg).toFixed(3)} ${cx} ${cy})"`;
return [
`<svg viewBox="0 0 300 270" width="100%" role="img" aria-label="받침 ${d.no} 평면: 블럭 ${d.blockMm}, 몰탈패드 ${d.padMm}, 받침 ${d.A} × ${d.B}, 솔플레이트 ${d.L} × ${d.W}, 회전 ${d.rotDeg.toFixed(2)}°">`,
`<g ${turn}>`,
square(cx, cy, d.blockMm * k, d.blockMm * k, `fill="#dfe5ec" stroke="${MUTED}"`),
square(cx, cy, d.padMm * k, d.padMm * k, `fill="none" stroke="${MUTED}" stroke-dasharray="4 3"`),
square(cx, cy, d.L * k, d.W * k, `fill="none" stroke="${ORANGE}" stroke-dasharray="5 3"`),
square(cx, cy, d.A * k, d.B * k, `fill="#b9c3ce" stroke="${INK}"`),
`</g>`,
text(cx, cy - (d.blockMm * k) / 2 - 8, `${d.blockMm}`),
text(cx, cy + 4, `${d.A} × ${d.B}`),
text(cx, cy + (d.blockMm * k) / 2 + 16, `솔플 ${d.L} × ${d.W} · 패드 ${d.padMm}`, 'middle', MUTED),
text(cx, 262, `받침 ${d.no} · 회전 ${d.rotDeg.toFixed(2)}° (방위각)`, 'middle', MUTED),
'</svg>',
].join('');
}
/** 교축 방향 단면 (높이 2배 과장): 블럭 h1, 패드 h2, 받침 h3, 솔플레이트 h4. E = h1 + h2 + h3 + h4. */
export function abutBearingSectionSvg(d: AbutBearingDetail): string {
const kx = 0.17;
const ky = 0.34; // 2× exaggerated heights
const cx = 150;
const base = 215;
const E = d.h1 + d.h2 + d.h3 + d.h4;
const layer = (bottom: number, h: number, w: number, style: string) =>
`<rect x="${(cx - (w * kx) / 2).toFixed(1)}" y="${(base - (bottom + h) * ky).toFixed(1)}" width="${(w * kx).toFixed(1)}" height="${(h * ky).toFixed(1)}" ${style}/>`;
const at = (mm: number) => base - mm * ky;
const right = cx + (d.blockMm * kx) / 2 + 6;
return [
`<svg viewBox="0 0 330 250" width="100%" role="img" aria-label="받침 ${d.no} 단면: h1 ${d.h1}, h2 ${d.h2}, h3 ${d.h3}, h4 ${d.h4}, E ${E}">`,
`<line x1="20" y1="${base}" x2="300" y2="${base}" stroke="${MUTED}"/>`,
layer(0, d.h1, d.blockMm, `fill="#dfe5ec" stroke="${MUTED}"`),
layer(d.h1, d.h2, d.padMm, `fill="#c9d1da" stroke="${MUTED}"`),
layer(d.h1 + d.h2, d.h3, d.A, `fill="#b9c3ce" stroke="${INK}"`),
layer(d.h1 + d.h2 + d.h3, d.h4, d.L, `fill="#f0c9a0" stroke="${ORANGE}"`),
text(right, at(d.h1 / 2) + 4, `h1 ${d.h1}`, 'start'),
text(right, at(d.h1 + d.h2 / 2) + 4, `h2 ${d.h2}`, 'start'),
text(right, at(d.h1 + d.h2 + d.h3 / 2) + 4, `h3 ${d.h3}`, 'start'),
text(right, at(E - d.h4 / 2) + 4, `h4 ${d.h4}`, 'start'),
text(cx, at(E) - 10, `E = ${E}`),
text(24, base + 16, `교좌면 ${d.seatEl.toFixed(3)}`, 'start', MUTED),
text(24, at(d.h1 + d.h2) - 4, `z ${d.z.toFixed(3)}`, 'start', MUTED),
text(cx, 244, '교축 방향 · 높이 2배 과장', 'middle', MUTED),
'</svg>',
].join('');
}
/** 솔플레이트 상세: 길이 L, 두께 T1 → T2 (경사 S). 두께는 과장해서 그린다. */
export function abutSolplateSvg(d: AbutBearingDetail): string {
const kx = 0.3;
const ky = 2.2; // thickness exaggerated
const x0 = 45;
const x1 = x0 + d.L * kx;
const base = 120;
return [
`<svg viewBox="0 0 330 190" width="100%" role="img" aria-label="받침 ${d.no} 솔플레이트: L ${d.L}, T1 ${d.T1}, T2 ${d.T2}, S ${d.S.toFixed(3)} %">`,
`<polygon points="${x0},${base} ${x1.toFixed(1)},${base} ${x1.toFixed(1)},${(base - d.T2 * ky).toFixed(1)} ${x0},${(base - d.T1 * ky).toFixed(1)}" fill="#f0c9a0" stroke="${ORANGE}"/>`,
text(x0 - 4, base - (d.T1 * ky) / 2 + 4, `T1 ${d.T1}`, 'end'),
text(x1 + 4, base - (d.T2 * ky) / 2 + 4, `T2 ${d.T2}`, 'start'),
text((x0 + x1) / 2, base + 18, `L ${d.L} · W ${d.W}`),
text((x0 + x1) / 2, base - Math.max(d.T1, d.T2) * ky - 10, `S ${d.S.toFixed(3)} % · T ${d.h4}`),
text(165, 180, '솔플레이트 상세 · 두께 과장 · 가정 (QA-15)', 'middle', MUTED),
'</svg>',
].join('');
}