diff --git a/src/core/abut/rebar/block.ts b/src/core/abut/rebar/block.ts
index 3109576..5c3b2ab 100644
--- a/src/core/abut/rebar/block.ts
+++ b/src/core/abut/rebar/block.ts
@@ -88,6 +88,8 @@ export interface AbutBlockPath {
export interface AbutBlockRebar {
settings: AbutBlockSettings;
concreteH: number;
+ /** 교좌면 높이 (mm, 저판 하면 기준). 경로 z에서 빼면 교좌면 기준. */
+ seatZ: number;
depth: { S1: number; S2: number };
embedment: { S1: number; S2: number };
paths: AbutBlockPath[];
@@ -184,5 +186,6 @@ export function abutBlockRebar(
basis: '규칙',
modified: s.spacingS2 !== ABUT_BLOCK_REBAR_DEFAULT.spacingS2,
});
- return { settings: s, concreteH, depth, embedment, paths, bars, issues };
+ const seatZ = blocks[0] ? (blocks[0].seatEl - bottomEl) * 1000 : 0;
+ return { settings: s, concreteH, seatZ, depth, embedment, paths, bars, issues };
}
diff --git a/src/view/abut-block-rebar-svg.ts b/src/view/abut-block-rebar-svg.ts
new file mode 100644
index 0000000..362146a
--- /dev/null
+++ b/src/view/abut-block-rebar-svg.ts
@@ -0,0 +1,73 @@
+/**
+ * 교대 받침블럭 배근 삽도 (2026-11-05, 시안 ③ 받침블럭 카드, 받침블럭철근.md §8): 평면 (블럭 1개, S2 실선 · S1 파선),
+ * 단면 (교대축 방향 절단: S1 아래 줄, S2 위 줄, 다리, 블럭 · 패드 · 교좌면). 순수 문자열 함수. 철근은 block.ts paths에서 그린다.
+ * 색: S1 (주철근, 아래) #0F5C66, S2 (보조, 위) #B4531F.
+ */
+import type { AbutBlockRebar, AbutBlockPath } from '../core/abut/rebar/block';
+import { balloon, CONC, MUTED, NO_PICK, opacity, text, WALL, type AbutRebarPick } from './abut-footing-rebar-svg';
+
+export const ABUT_BLOCK_COLORS = { s1: '#0F5C66', s2: '#B4531F' } as const;
+const C = ABUT_BLOCK_COLORS;
+
+/** 블럭 한 개의 경로만 쓴다 (첫 블럭, 방위각 0 가정). */
+function firstBlock(paths: AbutBlockPath[]): AbutBlockPath[] {
+ const b = paths[0]?.block;
+ return paths.filter((p) => p.block === b);
+}
+
+/** 평면: 블럭 1200 × 1200. S2 (교대축 간격, 교대축 직각으로 뻗음) 실선, S1 (교대축 직각 간격, 교대축 방향) 파선. */
+export function abutBlockPlanSvg(r: AbutBlockRebar, pick: AbutRebarPick = NO_PICK, blockCenter: [number, number] = [0, 0]): string {
+ const ps = firstBlock(r.paths);
+ const k = 0.2;
+ const X = (s: number) => 40 + (s - blockCenter[0] + 600) * k;
+ const Y = (t: number) => 40 + (blockCenter[1] + 600 - t) * k;
+ const parts: string[] = [];
+ parts.push(``);
+ for (const p of ps) {
+ const a = p.pts[1];
+ const b = p.pts[2];
+ const dashed = p.mark === 'S1' ? ' stroke-dasharray="4 3"' : '';
+ parts.push(``);
+ }
+ parts.push(text(40 + 600 * k, 40 + 1200 * k + 16, '블럭 1200 × 1200 · S2 실선 (위) · S1 파선 (아래) · 교대축 → ', 'middle', MUTED, 10));
+ parts.push(balloon(40 - 16, 40 + 600 * k, 'S2', C.s2, pick), balloon(40 + 1200 * k + 16, 40 + 600 * k, 'S1', C.s1, pick));
+ const W = 40 + 1200 * k + 60;
+ const H = 40 + 1200 * k + 40;
+ return ``;
+}
+
+/** 단면 (교대축 방향 절단, 블럭 가운데): 가로 s (블럭 안), 세로 z (교좌면 0, 블럭 상면 = 콘크리트 높이, 패드 위 125). */
+export function abutBlockSectionSvg(r: AbutBlockRebar, pick: AbutRebarPick = NO_PICK): string {
+ const ps = firstBlock(r.paths).map((p) => ({ ...p, pts: p.pts.map(([s, t, z]) => [s, t, z - r.seatZ] as [number, number, number]) }));
+ const k = 0.42;
+ const kz = 0.42;
+ const zTop = r.concreteH + 50;
+ const zBot = Math.min(...ps.flatMap((p) => p.pts.map((q) => q[2])), 0) - 60;
+ const sLeft = Math.min(...ps.flatMap((p) => p.pts.map((q) => q[0])));
+ const X = (s: number) => 50 + (s - sLeft) * k;
+ const Y = (z: number) => 40 + (zTop - z) * kz;
+ const W = 50 + 1200 * k + 60;
+ const H = Y(zBot) + 40;
+ const parts: string[] = [];
+ const blockL = Math.min(...ps.flatMap((p) => p.pts.map((q) => q[0])));
+ parts.push(``);
+ parts.push(``);
+ parts.push(text(X(blockL + 50) - 6, Y(zTop) + 4, '몰탈패드 50', 'end', MUTED, 10));
+ parts.push(``);
+ parts.push(text(X(blockL + 1300) + 6, Y(0) + 4, '교좌면', 'start', MUTED, 10));
+ for (const p of ps) {
+ const color = p.mark === 'S1' ? C.s1 : C.s2;
+ if (p.mark === 'S1') {
+ // S1: 교대축 방향 줄 (단면 가운데에서 보면 점 · 선). 다리 두 개를 같이 그린다.
+ const pts = p.pts.map(([s, , zz]) => `${X(s).toFixed(1)},${Y(zz).toFixed(1)}`).join(' ');
+ parts.push(``);
+ } else {
+ // S2: 교대축 간격으로 놓인 본 — 단면에서 점.
+ const s = p.pts[1][0];
+ parts.push(``);
+ }
+ }
+ parts.push(text(X(blockL + 600), Y(zBot) + 14, `블럭 콘크리트 ${r.concreteH} · 묻힘 S2 ${Math.round(r.embedment.S2)} · S1 ${Math.round(r.embedment.S1)}`, 'middle', MUTED, 10));
+ parts.push(balloon(X(blockL + 600), Y(r.depth.S2 + 0) - 10, 'S2', C.s2, pick), balloon(X(blockL + 600), Y(0) - 22, 'S1', C.s1, pick));
+ return ``;
+}
diff --git a/tests/abut-rebar-block.test.ts b/tests/abut-rebar-block.test.ts
index fe0f763..b43b71c 100644
--- a/tests/abut-rebar-block.test.ts
+++ b/tests/abut-rebar-block.test.ts
@@ -72,3 +72,17 @@ describe('③ 받침블럭 배근 입력 저장', () => {
expect(() => checkStoredAbutments([{ ...rec, rebar: { block: { coverTop: 'x' } } as never }])).toThrow('받침블럭 배근');
});
});
+
+import { abutBlockPlanSvg, abutBlockSectionSvg } from '../src/view/abut-block-rebar-svg';
+
+describe('③ 받침블럭 삽도', () => {
+ it('평면: S1 · S2 번호 풍선, 단면: 묻힘 치수 문자열 · 몰탈패드 · 교좌면; 선택하면 나머지 흐리게 / 숨김', () => {
+ const p = abutBlockPlanSvg(A1);
+ expect(p).toContain('data-pick="S1"');
+ expect(p).toContain('data-pick="S2"');
+ const s = abutBlockSectionSvg(A1);
+ expect(s).toContain('블럭 콘크리트 75 · 묻힘 S2 675 · S1 688');
+ expect(s).toContain('몰탈패드 50');
+ expect(abutBlockSectionSvg(A1, { marks: new Set(['S2']), only: true })).toContain('opacity="0"');
+ });
+});
diff --git a/verify/dump/abut-block-svg.dump.ts b/verify/dump/abut-block-svg.dump.ts
new file mode 100644
index 0000000..3cc1e5b
--- /dev/null
+++ b/verify/dump/abut-block-svg.dump.ts
@@ -0,0 +1,16 @@
+// 교대 A1 받침블럭 배근 삽도 2장을 SVG로 저장한다 (PNG 확인: python3 -I ../verify/svg_to_png.py