From dd590b899c4457cdb2304d94724ac1bdd691a00a Mon Sep 17 00:00:00 2001 From: ypshim Date: Mon, 26 Oct 2026 14:18:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(rebar):=20plan=20view=20of=20the=20block?= =?UTF-8?q?=20rebar=20with=20the=20=EB=8B=A8=EB=A9=B4/=ED=8F=89=EB=A9=B4?= =?UTF-8?q?=20switch=20(=EC=8A=B9=EC=9D=B8=202026-10-26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/main.ts | 21 ++++++++- src/view/block-rebar-plan-svg.ts | 76 ++++++++++++++++++++++++++++++ tests/block-rebar-plan-svg.test.ts | 28 +++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/view/block-rebar-plan-svg.ts create mode 100644 tests/block-rebar-plan-svg.test.ts diff --git a/src/main.ts b/src/main.ts index 1e29773..e7006f6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 diff --git a/src/view/block-rebar-plan-svg.ts b/src/view/block-rebar-plan-svg.ts new file mode 100644 index 0000000..3dd7dc1 --- /dev/null +++ b/src/view/block-rebar-plan-svg.ts @@ -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(``); + // 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(``); + parts.push(``); + } + } + // 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(``); + parts.push(``); + } + } + 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(`S2`); + parts.push(`S1`); + parts.push(`${INTEGRATED_BLOCK.length}`); + } 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(`S2`); + parts.push(`S1`); + } + return `${parts.join('')}`; +} diff --git a/tests/block-rebar-plan-svg.test.ts b/tests/block-rebar-plan-svg.test.ts new file mode 100644 index 0000000..0109a95 --- /dev/null +++ b/tests/block-rebar-plan-svg.test.ts @@ -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, / { + 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, / { + const svg = blockRebarPlanSvg(buildPier(loadP1()), 0); + expect(svg).toContain('>S2<'); + expect(svg).toContain('>S1<'); + }); +});