fix(abut): 교대 받침 공통 규칙을 교각 솔플레이트 규칙 형식으로 변경

- 이전 가정(받침 A×B 700×640, h3 181, T 30, 여유 100)을 없애고, 교각과 같은 솔플레이트 규칙(src/core/solplate-rule.ts)을 씀 (결정 2026-11-03: 교각 규칙을 기본값으로)
- 입력: 솔플레이트 교직 L 1000, 교축 W 1100, 종단 기울기 S −0.5 %. 받침높이 H는 교대 받침 시트 값(A1 275 mm)으로 고정
- 계산: T = ⌈22 + L·|S|/200⌉ = 25, h3 = H − T = 250, T1 27.5 / T2 22.5, E = 400. 교각 카드와 같은 안내 줄 표시
- 받침 종류 · 용량 입력은 유지. ELEVATION TABLE의 A·B 열 삭제, 삽도는 솔플레이트 L × W 기준
- 테스트 갱신. 전체 651건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-03 18:37:17 +09:00
1 parent 4b02dde153
commit 6b969f4a7c
7 files changed
+137 -110

No files matched your search

+46 -39
View File
@@ -1,29 +1,29 @@
/**
* 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수.
*
* - 교대 받침 상세도가 없어 받침 A × B, h3, 솔플레이트는 교각 P1 전열 값을 가정한다 (spec_abut QA-15).
* - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 (받침) + h4 (솔플레이트 T). z = 패드 상면 = EL.X (결정 2026-11-03).
* - 솔플레이트 경사 S = 종단경사 i. 두께 T1·T2 = T ± L·|S| / 2 (반올림), 검산 T1 − T2 ≈ L·|S|.
* - 솔플레이트는 교각과 같은 규칙(C안, src/core/solplate-rule.ts)을 쓴다: 받침높이 H(솔플레이트 포함)는 교대 받침 시트 값
* (운곡교 A1 275 mm)으로 고정하고, 교직 L · 교축 W · 종단 기울기 S를 입력한다. 중심 두께 T = ⌈22 + L·|S|/200⌉,
* T1·T2 = T ± L·|S|/200, 받침 본체 h3 = H − T. 기본값은 교각과 같은 1000 × 1100 · −0.5 % (가정, QA-15, 결정 2026-11-03).
* - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T) = h1 + h2 + H. z = 패드 상면 = EL.X.
*/
import { solplatePlan, solplateRuleIssues } from '../solplate-rule';
import type { SolplatePlan, SolplateRule } from '../types';
import { ABUT_PAD_MM, type AbutModel } from './geometry';
import type { AbutMembers } from './members';
export interface AbutBearingRule {
/** 받침 높이 h3 (mm). */
h3Mm: number;
/** 솔플레이트 두께 T (mm) = h4. */
solplateTMm: number;
/** 솔플레이트 여유 L − A, W − B (mm). */
solplateMarginMm: number;
/** 솔플레이트 교직 L (mm). */
lengthMm: number;
/** 솔플레이트 교축 W (mm). */
widthMm: number;
/** 종단 기울기 S (%). */
slopePct: number;
/** 받침 종류 · 용량 (글자 입력). */
kind: string;
}
/** 기본값: 교각 P1 전열 값 (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */
export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { h3Mm: 181, solplateTMm: 30, solplateMarginMm: 100, kind: '탄성받침' };
/** 받침 A × B (mm), 교각 P1 전열 가정 (QA-15). */
export const ABUT_BEARING_A_MM = 700;
export const ABUT_BEARING_B_MM = 640;
/** 기본값: 교각 솔플레이트 규칙과 같은 1000 × 1100 · −0.5 % (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */
export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { lengthMm: 1000, widthMm: 1100, slopePct: -0.5, kind: '탄성받침' };
/** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */
export const ABUT_BLOCK_MM = 1200;
export const ABUT_PAD_PLAN_MM = 1000;
@@ -36,6 +36,15 @@ export function resolveBearingRule(changes?: Partial<AbutBearingRule>): AbutBear
return { ...ABUT_BEARING_RULE_DEFAULT, ...(changes ?? {}) };
}
/** The pier rule with the 교대 받침높이 H (from the 받침 sheet). */
export function abutSolplateRule(rule: AbutBearingRule, heightMm: number): SolplateRule {
return { heightMm, lengthMm: rule.lengthMm, widthMm: rule.widthMm, slopePct: rule.slopePct };
}
export function abutSolplatePlan(rule: AbutBearingRule, heightMm: number): SolplatePlan {
return solplatePlan(abutSolplateRule(rule, heightMm));
}
export interface AbutElevationRow {
no: number;
/** IP3(① 쪽)에서 잰 위치 (m). */
@@ -51,8 +60,6 @@ export interface AbutElevationRow {
E: number;
/** 솔플레이트 상면 EL (m) = 교좌면 + E. */
solplateTopEl: number;
A: number;
B: number;
/** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */
stepGapM: number | null;
/** 받침 방위각 (deg): CAD (엑셀 값, 동 = 0° 반시계) / 진북 (북에서 시계). 입력이 없으면 null. */
@@ -62,11 +69,12 @@ export interface AbutElevationRow {
rotDeg: number;
}
export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): AbutElevationRow[] {
export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutElevationRow[] {
const half = ABUT_BLOCK_MM / 2000;
const plan = abutSolplatePlan(rule, heightMm);
return model.bearings.map((b) => {
const h1 = b.blockHeightMm - ABUT_PAD_MM;
const E = h1 + ABUT_PAD_MM + rule.h3Mm + rule.solplateTMm;
const E = h1 + ABUT_PAD_MM + plan.h3 + plan.T;
const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half);
return {
no: b.no,
@@ -76,12 +84,10 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): Abut
h1,
h2: ABUT_PAD_MM,
z: b.z,
h3: rule.h3Mm,
h4: rule.solplateTMm,
h3: plan.h3,
h4: plan.T,
E,
solplateTopEl: b.seatEl + E / 1000,
A: ABUT_BEARING_A_MM,
B: ABUT_BEARING_B_MM,
stepGapM: gaps.length ? Math.min(...gaps) : null,
azimuthCadDeg: b.azimuthDeg === undefined ? null : (((90 - b.azimuthDeg) % 360) + 360) % 360,
azimuthNorthDeg: b.azimuthDeg === undefined ? null : ((b.azimuthDeg % 360) + 360) % 360,
@@ -92,38 +98,39 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): Abut
export interface AbutSolplateRow {
no: number;
/** 경사 (%) = 종단경사 i. */
/** 기울기 (%). */
S: number;
L: number;
W: number;
T: number;
T1: number;
T2: number;
/** 검산 문구 "T1 − T2 ≈ L·|S|". */
/** 검산 문구 "T1 − T2 = L·|S|/100". */
check: string;
}
export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, slopeI: number): AbutSolplateRow[] {
const L = ABUT_BEARING_A_MM + rule.solplateMarginMm;
const W = ABUT_BEARING_B_MM + rule.solplateMarginMm;
const rise = L * Math.abs(slopeI);
const T1 = Math.round(rule.solplateTMm + rise / 2);
const T2 = Math.round(rule.solplateTMm - rise / 2);
export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutSolplateRow[] {
const p = abutSolplatePlan(rule, heightMm);
return model.bearings.map((b) => ({
no: b.no,
S: slopeI * 100,
L,
W,
T: rule.solplateTMm,
T1,
T2,
check: `${T1 - T2} ≈ ${rise.toFixed(1)}`,
S: p.slopePct,
L: p.L,
W: p.W,
T: p.T,
T1: p.T1,
T2: p.T2,
check: `${(p.T1 - p.T2).toFixed(1)} = ${((p.L * Math.abs(p.slopePct)) / 100).toFixed(1)}`,
}));
}
/** Input problems of the 교대 받침 규칙 (the pier rule's checks with the 교대 H). Empty when it can build a solplate. */
export function abutBearingRuleIssues(rule: AbutBearingRule, heightMm: number): string[] {
return solplateRuleIssues(abutSolplateRule(rule, heightMm));
}
/** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */
export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule): string[] {
const rows = abutElevationRows(model, rule);
export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number): string[] {
const rows = abutElevationRows(model, rule, heightMm);
const minH1 = Math.min(...rows.map((r) => r.h1));
const half = ABUT_BLOCK_MM / 2000;
// Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front.
+15 -10
View File
@@ -5,7 +5,7 @@
import type { AbutRecord } from './parse';
import type { WingForm } from './geometry';
import { shapeIssues, type AbutShape } from './shape';
import type { AbutBearingRule } from './bearing-table';
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
@@ -87,14 +87,17 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm, 계산 미사용)' },
];
/** 받침 규칙 입력칸 (② 교량받침 카드, 시안 공통 · 규칙). 기본값은 교각 P1 전열 값 (가정, QA-15). */
export const BEARING_RULE_FIELDS: { key: 'h3Mm' | 'solplateTMm' | 'solplateMarginMm'; label: string }[] = [
{ key: 'h3Mm', label: '받침 높이 h3 (mm)' },
{ key: 'solplateTMm', label: '솔플레이트 T (mm)' },
{ key: 'solplateMarginMm', label: '솔플 여유 L − A, W − B (mm)' },
/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */
export const BEARING_RULE_FIELDS: { key: 'lengthMm' | 'widthMm' | 'slopePct'; label: string }[] = [
{ key: 'lengthMm', label: '솔플레이트 교직 L (mm)' },
{ key: 'widthMm', label: '솔플레이트 교축 W (mm)' },
{ key: 'slopePct', label: '종단 기울기 S (%)' },
];
/** Changes one 받침 규칙 value: the numbers must be greater than 0, the 받침 종류 must not be empty. */
/**
* Changes one 받침 규칙 value. L and W must be greater than 0, S any number, and the rule must still build a solplate with
* the 교대 받침높이 H (the pier rule's checks). The 받침 종류 must not be empty.
*/
export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | string): AbutRecord {
if (key === 'kind') {
const kind = String(value).trim();
@@ -102,7 +105,9 @@ export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, va
return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), kind } };
}
const n = Number(value);
if (!(Number.isFinite(n) && n > 0)) throw new Error('받침 규칙 값은 0보다 큰 숫자여야 합니다.');
return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), [key]: n } };
if (!Number.isFinite(n)) throw new Error('솔플레이트 규칙 값은 숫자여야 합니다.');
const bearingRule = { ...(rec.bearingRule ?? {}), [key]: n };
const issues = abutBearingRuleIssues(resolveBearingRule(bearingRule), rec.bearingHeightMm);
if (issues.length > 0) throw new Error(issues[0]);
return { ...rec, bearingRule };
}
+28 -15
View File
@@ -18,6 +18,7 @@ import {
ABUT_PAD_PLAN_MM,
abutBearingChecks,
abutElevationRows,
abutSolplatePlan,
abutSolplateRows,
resolveBearingRule,
} from './core/abut/bearing-table';
@@ -68,7 +69,7 @@ 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_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
import { SOLPLATE_MIN_T2_MM, SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
import type { SolplateRule } from './core/types';
import { type BarSummary, elevationRows, solplateRows } from './core/bearing-table';
@@ -805,8 +806,10 @@ function fillAbutBearingCard(
return;
}
const m3 = (v: number) => v.toFixed(3);
const elev = abutElevationRows(model, rule);
const sol = abutSolplateRows(model, rule, rec.slopeI);
const H = rec.bearingHeightMm;
const plan = abutSolplatePlan(rule, H);
const elev = abutElevationRows(model, rule, H);
const sol = abutSolplateRows(model, rule, H);
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
const pickNo = elev.some((r) => r.no === state.abutBearingNo) ? state.abutBearingNo : elev[0].no;
const picker = el('div', undefined, 'abut-bearing-pick');
@@ -825,14 +828,14 @@ function fillAbutBearingCard(
const er = elev.find((r) => r.no === pickNo)!;
const sr = sol.find((r) => r.no === pickNo)!;
const detail: AbutBearingDetail = {
no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, A: er.A, B: er.B, L: sr.L, W: sr.W,
no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, L: sr.L, W: sr.W,
h1: er.h1, h2: er.h2, h3: er.h3, h4: er.h4, T1: sr.T1, T2: sr.T2, rotDeg: er.rotDeg, S: sr.S, seatEl: er.seatEl, z: er.z,
};
const figs = el('div', undefined, 'abut-bearing-figs');
for (const [svg, caption] of [
[abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 점선 = 솔플레이트'],
[abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 = 솔플레이트 L × W'],
[abutBearingSectionSvg(detail), '단면 · 교축 방향 · E = h1 + h2 + h3 + h4'],
[abutSolplateSvg(detail), '솔플레이트 상세 · 가정'],
[abutSolplateSvg(detail), '솔플레이트 상세 · T = ⌈22 + L·|S|/200⌉'],
]) {
const fig = el('figure', undefined, 'abut-fig');
fig.innerHTML = svg;
@@ -840,26 +843,28 @@ function fillAbutBearingCard(
figs.append(fig);
}
box.append(figs);
box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl'));
box.append(el('p', `받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3 = H − T, h4 = T (솔플레이트 규칙)`, 'lbl'));
box.append(
table(
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'],
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'],
elev.map((r) => [
String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4),
String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM),
String(r.E), m3(r.solplateTopEl), r.stepGapM === null ? '—' : m3(r.stepGapM),
r.azimuthCadDeg === null || r.azimuthNorthDeg === null ? '입력 없음' : `${r.azimuthCadDeg.toFixed(3)} / ${r.azimuthNorthDeg.toFixed(3)}`,
r.rotDeg.toFixed(2),
]),
),
);
box.append(el('p', '솔플레이트 상세 TABLE (mm) · 가정 (교각 P1 값)', 'lbl'));
box.append(el('p', '솔플레이트 상세 TABLE (mm) · 솔플레이트 규칙', 'lbl'));
box.append(
table(
['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 ≈ L·|S|'],
sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 = L·|S|/100'],
sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), r.T1.toFixed(1), r.T2.toFixed(1), r.check]),
),
);
box.append(el('p', `공통 · 규칙 — 블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small'));
// 솔플레이트 규칙 (교각 카드와 같은 형식, 2026-11-03): H는 교대 받침 시트 값으로 고정, L · W · S를 입력한다.
const d = ABUT_BEARING_RULE_DEFAULT;
box.append(el('p', `솔플레이트 규칙 (이 교대 · 기본 ${d.lengthMm} × ${d.widthMm} · ${d.slopePct} %) · 받침높이 H ${H} mm 고정 (솔플레이트 포함)`, 'lbl'));
for (const f of BEARING_RULE_FIELDS) {
const id = idOf(`rule-${f.key}`);
const lab = el('label', f.label);
@@ -873,8 +878,16 @@ function fillAbutBearingCard(
fail(err, input, String(rule[f.key]));
}
});
box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]} · 가정`, 'muted small'));
box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]}`, 'muted small'));
}
box.append(
el(
'p',
`중심 두께 T = ${plan.T} mm (T2 ≥ ${SOLPLATE_MIN_T2_MM} mm) · 받침 본체 h3 = H − T = ${plan.h3} mm · T1 = ${plan.T1.toFixed(1)} mm · T2 = ${plan.T2.toFixed(1)} mm`,
'muted small',
),
);
box.append(el('p', `블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small'));
const kindId = idOf('rule-kind');
const kindLab = el('label', '받침 종류 · 용량');
kindLab.htmlFor = kindId;
@@ -890,7 +903,7 @@ function fillAbutBearingCard(
}
});
box.append(kindLab, kind, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT.kind}`, 'muted small'));
box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule).join(' · ')}`, 'muted small abut-checks'));
box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule, H).join(' · ')}`, 'muted small abut-checks'));
}
/** Shows only the card of the open row in the ② 교대 editor; the basic items stay visible. */
+7 -10
View File
@@ -8,9 +8,7 @@ export interface AbutBearingDetail {
/** 블럭 평면 (정사각), 몰탈패드 평면 (정사각), mm. */
blockMm: number;
padMm: number;
/** 받침 A × B, 솔플레이트 L × W (mm). */
A: number;
B: number;
/** 솔플레이트 교직 L × 교축 W (mm). 받침 본체 평면은 입력이 없어 그리지 않는다. */
L: number;
W: number;
/** 높이 (mm): 블럭 콘크리트 h1, 패드 h2, 받침 h3, 솔플레이트 h4 = T. */
@@ -36,23 +34,22 @@ const text = (x: number, y: number, s: string, anchor = 'middle', fill = INK) =>
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. 블럭은 받침 방위각만큼 돈다. */
/** 평면: 블럭 1200, 몰탈패드 1000 (점선), 솔플레이트 L × W (주황). 블럭은 받침 방위각만큼 돈다. */
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)}°">`,
`<svg viewBox="0 0 300 270" width="100%" role="img" aria-label="받침 ${d.no} 평면: 블럭 ${d.blockMm}, 몰탈패드 ${d.padMm}, 솔플레이트 ${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}"`),
square(cx, cy, d.L * k, d.W * k, `fill="#f0c9a0" fill-opacity="0.6" stroke="${ORANGE}"`),
`</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, cy + 4, `솔플 ${d.L} × ${d.W}`),
text(cx, cy + (d.blockMm * k) / 2 + 16, `패드 ${d.padMm} (점선)`, 'middle', MUTED),
text(cx, 262, `받침 ${d.no} · 회전 ${d.rotDeg.toFixed(2)}° (방위각)`, 'middle', MUTED),
'</svg>',
].join('');
@@ -74,7 +71,7 @@ export function abutBearingSectionSvg(d: AbutBearingDetail): string {
`<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.L - 100, `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'),