feat(rebar): plan view of the block rebar with the 단면/평면 switch (승인 2026-10-26)
- view/block-rebar-plan-svg.ts: the selected pair from above — S2 lines along the 교각축 (spaced at S2 across the 교축), S1 lines along the 교축 (spaced at S1), each end a dot (leg); the bar sets carry the row numbers only (S1, S2). - The ③ card switches between the C–C section and the plan. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
521979b437
commit
dd590b899c
3 files changed
+123
-2
No files matched your search
+19
-2
@@ -23,6 +23,7 @@ import { bearingPlanSvg, type BearingPlanOverlay, PAIR_CHIP_LABELS } from './vie
|
||||
import { bearingSectionSvg, type BearingDrawingRef } from './view/bearing-section-svg';
|
||||
import { solplateDetailSvg, type SolplateDrawingRef } from './view/solplate-svg';
|
||||
import { blockRebarSectionSvg } from './view/block-rebar-svg';
|
||||
import { blockRebarPlanSvg } from './view/block-rebar-plan-svg';
|
||||
import { BLOCK_REBAR_RANGES } from './core/rebar/block-check';
|
||||
import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block';
|
||||
import { groundSectionSvg } from './view/ground-svg';
|
||||
@@ -1048,8 +1049,22 @@ function renderBlockRebarSettings(box: HTMLDivElement, pier: Pier): void {
|
||||
const n = pier.model.integratedBlocks.length;
|
||||
wrap.appendChild(el('p', `적용 블럭: ${n > 0 ? `일체 ${n}쌍` : `분리 ${pier.model.blocks.length}개`} (전체)`, 'muted small'));
|
||||
const fig = el('figure', undefined, 'ground-fig');
|
||||
fig.innerHTML = blockRebarSectionSvg(pier.model, Math.min(bearingPairIndex, Math.max(0, pairEccentricities(pier.model.blocks).length - 1)));
|
||||
fig.appendChild(el('figcaption', '단면 C–C · 교축 방향 · 선택한 쌍 · S2 점(상면), S1 선과 다리(⊓) · 치수 mm', 'muted small'));
|
||||
const pairIdx = Math.min(bearingPairIndex, Math.max(0, pairEccentricities(pier.model.blocks).length - 1));
|
||||
// 단면 C–C / 평면 (승인 2026-10-26): the two views of the same settings; the 평면 shows the legs as dots and the row numbers.
|
||||
const viewRow = el('div', undefined, 'opts');
|
||||
for (const [v, label] of [['section', '단면 C–C'], ['plan', '평면']] as const) {
|
||||
const b = el('button', label, 'btn small');
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(bearingRebarView === v));
|
||||
b.addEventListener('click', () => {
|
||||
bearingRebarView = v;
|
||||
renderAll();
|
||||
});
|
||||
viewRow.appendChild(b);
|
||||
}
|
||||
wrap.appendChild(viewRow);
|
||||
fig.innerHTML = bearingRebarView === 'plan' ? blockRebarPlanSvg(pier.model, pairIdx) : blockRebarSectionSvg(pier.model, pairIdx);
|
||||
fig.appendChild(el('figcaption', bearingRebarView === 'plan' ? '평면 · 선택한 쌍 · S2 선(교각축 방향), S1 선(교축 방향) · 다리는 점 · 번호는 표의 행 번호' : '단면 C–C · 교축 방향 · 선택한 쌍 · S2 점(상면), S1 선과 다리(⊓) · 치수 mm', 'muted small'));
|
||||
wrap.appendChild(fig);
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
@@ -1589,6 +1604,8 @@ function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
|
||||
/** 상세 보기 pair shown in the plan, and whether the drawing values are overlaid (screen state only, 승인 2026-10-26). */
|
||||
let bearingPairIndex = 0;
|
||||
let bearingOverlayOn = true;
|
||||
/** ③ 받침블럭 view: the C–C section or the plan (승인 2026-10-26). */
|
||||
let bearingRebarView: 'section' | 'plan' = 'section';
|
||||
|
||||
/**
|
||||
* Drawing values of one bearing for the overlays (승인 2026-10-26): the imported drawing row where it has the value
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { PierModel } from '../core/geometry';
|
||||
import { blockFramePoint } from '../core/geometry';
|
||||
import { barDepths, barsAcross, type BlockRebarSettings } from '../core/rebar/bearing-block';
|
||||
import { pairEccentricities } from '../core/bearing-layout';
|
||||
import { INTEGRATED_BLOCK } from '../core/integrated-block';
|
||||
|
||||
/**
|
||||
* 받침블럭 배근 · 평면 (승인 2026-10-26): the blocks seen from above for the selected pair. x = 교축 (n), y = 교각축 (s).
|
||||
* S2 bars run along the 교각축 (lines spaced across the 교축 at S2), S1 bars run along the 교축 (spaced along the 교각축 at S1).
|
||||
* Each bar's ends are dots (the legs). The bar sets carry the table row numbers only (S1, S2).
|
||||
*/
|
||||
export function blockRebarPlanSvg(m: PierModel, pairIndex = 0): string {
|
||||
const s: BlockRebarSettings = m.blockRebar;
|
||||
const depth = barDepths(s);
|
||||
const pair = pairEccentricities(m.blocks)[pairIndex];
|
||||
if (!pair) return '';
|
||||
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
|
||||
const k = m.blocks.find((b) => b.no === pair.backNo && b.row === '후열')!;
|
||||
const ib = m.integratedBlocks.find((x) => x.frontNo === pair.frontNo);
|
||||
const sc = ib ? ib.s : (f.s + k.s) / 2;
|
||||
const nc = ib ? ib.n : (f.n + k.n) / 2;
|
||||
const X = (n: number) => (n - nc) * 1000;
|
||||
const Y = (sv: number) => (sv - sc) * 1000;
|
||||
const parts: string[] = [];
|
||||
|
||||
// One block: a = along the 교각축 (s), c = along the 교축 (n), both in metres from the block centre, turned by the row skew.
|
||||
const drawBlock = (cs: number, cn: number, skew: number, wA: number, wC: number) => {
|
||||
const P = (a: number, c: number): [number, number] => {
|
||||
const [sv, nv] = blockFramePoint({ s: cs, n: cn, rowSkewDeg: skew }, a, c);
|
||||
return [X(nv), Y(sv)];
|
||||
};
|
||||
// Outline
|
||||
const corners = [P(-wA / 2, -wC / 2), P(wA / 2, -wC / 2), P(wA / 2, wC / 2), P(-wA / 2, wC / 2)];
|
||||
parts.push(`<polygon points="${corners.map((p) => p.join(',')).join(' ')}" fill="none" stroke="#6E8496" stroke-width="10"></polygon>`);
|
||||
// S2: lines along the 교각축 at offsets across the 교축.
|
||||
{
|
||||
const a2s = barsAcross(wC * 1000, s.spacingS2, s);
|
||||
const half = (wA * 1000 - 2 * s.coverSide) / 2000;
|
||||
for (let i = 0; i < a2s.count; i++) {
|
||||
const c = (a2s.firstEnd + i * a2s.spacing) / 1000 - wC / 2;
|
||||
const p1 = P(-half, c);
|
||||
const p2 = P(half, c);
|
||||
parts.push(`<line x1="${p1[0]}" y1="${p1[1]}" x2="${p2[0]}" y2="${p2[1]}" stroke="#0F5C66" stroke-width="8"></line>`);
|
||||
parts.push(`<circle cx="${p1[0]}" cy="${p1[1]}" r="14" fill="#0F5C66"></circle><circle cx="${p2[0]}" cy="${p2[1]}" r="14" fill="#0F5C66"></circle>`);
|
||||
}
|
||||
}
|
||||
// S1: lines along the 교축 at offsets along the 교각축.
|
||||
{
|
||||
const a1s = barsAcross(wA * 1000, s.spacingS1, s);
|
||||
const half = (wC * 1000 - 2 * s.coverSide) / 2000;
|
||||
for (let i = 0; i < a1s.count; i++) {
|
||||
const a = (a1s.firstEnd + i * a1s.spacing) / 1000 - wA / 2;
|
||||
const p1 = P(a, -half);
|
||||
const p2 = P(a, half);
|
||||
parts.push(`<line x1="${p1[0]}" y1="${p1[1]}" x2="${p2[0]}" y2="${p2[1]}" stroke="#B4531F" stroke-width="8"></line>`);
|
||||
parts.push(`<circle cx="${p1[0]}" cy="${p1[1]}" r="14" fill="#B4531F"></circle><circle cx="${p2[0]}" cy="${p2[1]}" r="14" fill="#B4531F"></circle>`);
|
||||
}
|
||||
}
|
||||
return P;
|
||||
};
|
||||
|
||||
if (ib) {
|
||||
const P = drawBlock(ib.s, ib.n, ib.rowSkewDeg, INTEGRATED_BLOCK.depth / 1000, INTEGRATED_BLOCK.length / 1000);
|
||||
// Row numbers: S2 and S1 once each, at the first bar's end.
|
||||
const [sx, sy] = P(-0.6 + 0.1, -1.25 + 0.1);
|
||||
parts.push(`<text x="${sx}" y="${sy - 40}" font-size="90" fill="#0F5C66">S2</text>`);
|
||||
parts.push(`<text x="${sx + 200}" y="${sy + 150}" font-size="90" fill="#B4531F">S1</text>`);
|
||||
parts.push(`<text x="0" y="${-800}" text-anchor="middle" font-size="90" fill="#18212B">${INTEGRATED_BLOCK.length}</text>`);
|
||||
} else {
|
||||
for (const b of [f, k]) drawBlock(b.s, b.n, b.rowSkewDeg, 1.2, 1.2);
|
||||
const [sx, sy] = [X(f.n) - 500, Y(f.s) - 500];
|
||||
parts.push(`<text x="${sx}" y="${sy}" font-size="90" fill="#0F5C66">S2</text>`);
|
||||
parts.push(`<text x="${sx + 300}" y="${sy + 120}" font-size="90" fill="#B4531F">S1</text>`);
|
||||
}
|
||||
return `<svg viewBox="-1900 -1100 3800 2200" width="100%" style="max-width: 680px; height: auto" role="img" aria-label="받침블럭 배근 평면: S2 선과 S1 선, 다리는 점">${parts.join('')}</svg>`;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { blockRebarPlanSvg } from '../src/view/block-rebar-plan-svg';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
const count = (svg: string, re: RegExp) => (svg.match(re) ?? []).length;
|
||||
|
||||
describe('받침블럭 배근 평면 (승인 2026-10-26)', () => {
|
||||
it('a separate pair: 8 S2 and 8 S1 lines per block, each with two leg dots', () => {
|
||||
const svg = blockRebarPlanSvg(buildPier(loadP1()), 0);
|
||||
expect(count(svg, /stroke="#0F5C66" stroke-width="8"/g)).toBe(16);
|
||||
expect(count(svg, /stroke="#B4531F" stroke-width="8"/g)).toBe(16);
|
||||
expect(count(svg, /<circle/g)).toBe(64);
|
||||
});
|
||||
|
||||
it('a 일체 pair: 19 S2 lines and 8 S1 lines, with their leg dots', () => {
|
||||
const svg = blockRebarPlanSvg(buildPier({ ...loadP1(), bearingLayout: '일체' }), 0);
|
||||
expect(count(svg, /stroke="#0F5C66" stroke-width="8"/g)).toBe(19);
|
||||
expect(count(svg, /stroke="#B4531F" stroke-width="8"/g)).toBe(8);
|
||||
expect(count(svg, /<circle/g)).toBe(54);
|
||||
});
|
||||
|
||||
it('labels only the row numbers S1 and S2', () => {
|
||||
const svg = blockRebarPlanSvg(buildPier(loadP1()), 0);
|
||||
expect(svg).toContain('>S2<');
|
||||
expect(svg).toContain('>S1<');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user