feat(abut-rebar): 교대 받침블럭 배근 삽도 (평면 · 단면)
- 평면: 블럭 1200 × 1200, S2 실선 (위) · S1 파선 (아래), 번호 풍선 - 단면: 교좌면 기준, 몰탈패드 50, S1 다리 · S2 본, 묻힘 치수 문자열 - 결과에 교좌면 높이(seatZ) 추가 — 단면 축을 교좌면 기준으로 맞춤 - verify/dump: 받침블럭 삽도 덤프 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5856cfb423
commit
a68e4a0366
4 files changed
+107
-1
No files matched your search
@@ -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 };
|
||||
}
|
||||
@@ -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(`<rect x="${X(blockCenter[0] - 600).toFixed(1)}" y="${Y(blockCenter[1] + 600).toFixed(1)}" width="${(1200 * k).toFixed(1)}" height="${(1200 * k).toFixed(1)}" fill="${WALL}" stroke="${MUTED}"/>`);
|
||||
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(`<line x1="${X(a[0]).toFixed(1)}" y1="${Y(a[1]).toFixed(1)}" x2="${X(b[0]).toFixed(1)}" y2="${Y(b[1]).toFixed(1)}" stroke="${p.mark === 'S1' ? C.s1 : C.s2}" stroke-width="1.6"${dashed} opacity="${opacity(pick, [p.mark])}"/>`);
|
||||
}
|
||||
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 `<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" style="max-width:${Math.round(W * 1.4)}px" role="img" aria-label="받침블럭 배근 평면">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
/** 단면 (교대축 방향 절단, 블럭 가운데): 가로 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(`<rect x="${X(blockL + 50).toFixed(1)}" y="${Y(r.concreteH).toFixed(1)}" width="${(1200 * k).toFixed(1)}" height="${(r.concreteH * kz).toFixed(1)}" fill="${CONC}" stroke="${MUTED}"/>`);
|
||||
parts.push(`<rect x="${X(blockL + 50).toFixed(1)}" y="${Y(zTop).toFixed(1)}" width="${(1200 * k).toFixed(1)}" height="${(50 * kz).toFixed(1)}" fill="${WALL}" stroke="${MUTED}" stroke-dasharray="4 3"/>`);
|
||||
parts.push(text(X(blockL + 50) - 6, Y(zTop) + 4, '몰탈패드 50', 'end', MUTED, 10));
|
||||
parts.push(`<line x1="${X(blockL).toFixed(1)}" y1="${Y(0).toFixed(1)}" x2="${X(blockL + 1300).toFixed(1)}" y2="${Y(0).toFixed(1)}" stroke="${MUTED}" stroke-dasharray="6 3"/>`);
|
||||
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(`<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.4" opacity="${opacity(pick, ['S1'])}"/>`);
|
||||
} else {
|
||||
// S2: 교대축 간격으로 놓인 본 — 단면에서 점.
|
||||
const s = p.pts[1][0];
|
||||
parts.push(`<circle cx="${X(s).toFixed(1)}" cy="${Y(p.pts[1][2]).toFixed(1)}" r="2.6" fill="${color}" opacity="${opacity(pick, ['S2'])}"/>`);
|
||||
}
|
||||
}
|
||||
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 `<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" style="max-width:${Math.round(W * 1.2)}px" role="img" aria-label="받침블럭 배근 단면">${parts.join('')}</svg>`;
|
||||
}
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
// 교대 A1 받침블럭 배근 삽도 2장을 SVG로 저장한다 (PNG 확인: python3 -I ../verify/svg_to_png.py <svg> <png>).
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { it } from 'vitest';
|
||||
import { abutBlockRebar } from '../../src/core/abut/rebar/block';
|
||||
import { abutBlockPlanSvg, abutBlockSectionSvg } from '../../src/view/abut-block-rebar-svg';
|
||||
|
||||
it('dump 교대 받침블럭 삽도', () => {
|
||||
const out = process.env.DUMP_OUT ?? fileURLToPath(new URL('./out', import.meta.url));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const blocks = [1, 2, 3, 4, 5].map((no, i) => ({ no, s: 1 + i * 2, t: 0.2, rotDeg: 0, seatEl: 44.367, blockHeightMm: 125 }));
|
||||
const r = abutBlockRebar(blocks, 40.895);
|
||||
const ns = (svg: string) => svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
|
||||
writeFileSync(`${out}/block-plan.svg`, ns(abutBlockPlanSvg(r, undefined, [blocks[0].s * 1000, blocks[0].t * 1000])));
|
||||
writeFileSync(`${out}/block-section.svg`, ns(abutBlockSectionSvg(r)));
|
||||
});
|
||||
Reference in new issue
Block a user