From d5bf02e36bee6f0c887e3c293276e9c1425bfa58 Mon Sep 17 00:00:00 2001 From: ypshim Date: Mon, 26 Oct 2026 15:05:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(rebar):=20=EB=8B=A8=EB=A9=B4=C2=B7?= =?UTF-8?q?=ED=8F=90=ED=95=A9=20section=20with=20the=20top-row=20and=20sid?= =?UTF-8?q?e-row=20chains=20and=20bar-group=20bubbles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Top row (C1–C8) spacing along the 교축 from chainDims, between the coping's two long edges (±B/2). - Side rows (C12·C13) along the EL, from the coping top EL down to the lowest side bar. - One bubble per bar group cut at the station, with a leader to its bars. Co-Authored-By: Claude Haiku 5.5 --- src/main.ts | 2 +- src/view/coping-rebar-svg.ts | 83 ++++++++++++++++++++++++++++------ tests/coping-rebar-svg.test.ts | 11 +++-- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/main.ts b/src/main.ts index f44e97a..5e7f64c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1107,7 +1107,7 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void { const label = el('label', '단면 위치 s (m, 코핑 시작에서) '); label.appendChild(station); wrap.appendChild(label); - fig.innerHTML = copingSectionSvg(rebar3d.records, at); + fig.innerHTML = copingSectionSvg(rebar3d.records, at, pier.model.copingRefTop, pier.model.copingB * 1000); fig.appendChild(el('figcaption', `단면 · 폐합 구성 · s = ${at} m에서 자른 철근 (C9–C11 폐합 포함)`, 'muted small')); } wrap.appendChild(fig); diff --git a/src/view/coping-rebar-svg.ts b/src/view/coping-rebar-svg.ts index 86e66e1..aab646c 100644 --- a/src/view/coping-rebar-svg.ts +++ b/src/view/coping-rebar-svg.ts @@ -168,10 +168,12 @@ export function copingFrontSvg(records: BarRecord[], lengthMm: number, columnSm } /** - * 단면 · 폐합 구성 at the station s0 (m): the bars that cross it, seen along the coping length. - * A bar crossing s0 is drawn at the crossing point (n, EL); a bar along the 교축 at s0 is drawn as its line. + * 단면 · 폐합 구성 at the station s0 (m): the bars that cross it, seen along the coping length (승인 2026-10-26). + * x = 교축 (n, mm, centre 0), y = EL (mm, down). Dimensions: the top row spacing (3, chainDims over the crossings of the + * top row), the side-row spacing (4, from the coping top EL down to the lowest side bar), and the bubbles (3) with the + * bar-group labels, one per group. */ -export function copingSectionSvg(records: BarRecord[], s0: number): string { +export function copingSectionSvg(records: BarRecord[], s0: number, copingTopEl?: number, copingWidthMm?: number): string { const crossings: { name: string; x: number; y: number; group: keyof typeof COLOR }[] = []; const bars = records.filter(isCoping); const els = bars.flatMap((r) => r.pts.map((p) => p[1])); @@ -187,24 +189,79 @@ export function copingSectionSvg(records: BarRecord[], s0: number): string { crossings.push({ name: r.name, x: a[2] + (b[2] - a[2]) * t, y: a[1] + (b[1] - a[1]) * t, group: copingBarGroup(r.name) }); } } - // The same bar is recorded in several pieces, so the same spot can be crossed more than once: draw each spot once, - // and write each bar name once (at its first crossing), so the labels do not pile up. + // The same bar is recorded in several pieces, so draw each spot once. const seen = new Set(); - const named = new Set(); const unique = crossings.filter((c) => { const key = `${Math.round(c.x * 1e4)}|${Math.round(c.y * 1e4)}|${c.name}`; if (seen.has(key)) return false; seen.add(key); return true; }); + const Ymm = (el: number) => (top - el) * 1000; const xs = unique.map((c) => c.x * 1000); + const ys = unique.map((c) => Ymm(c.y)); const minX = Math.min(...xs, 0); const maxX = Math.max(...xs, 0); - const parts: string[] = unique.map((c) => { - const dot = ``; - if (named.has(c.name)) return dot; - named.add(c.name); - return `${dot}${c.name}`; - }); - return `${parts.join('')}`; + const minY = Math.min(...ys, 0); + const maxY = Math.max(...ys, 0); + const parts: string[] = []; + + for (const c of unique) { + parts.push(``); + } + + // 3: the top row (C1–C8) along the 교축, its spacing from chainDims over the crossing positions. + const topRow = unique.filter((c) => c.group === 'main' && Ymm(c.y) <= minY + 150); + const topStarts = [...new Set(topRow.map((c) => Math.round(c.x * 1000)))].sort((a, b) => a - b); + const halfW = (copingWidthMm ?? 3100) / 2; + const topChain = chainDims(topStarts, -halfW, halfW); + const dimTop = minY - 600; + for (const d of topChain) { + parts.push(`${d.text}`); + } + + // 4: the side rows (C12·C13) along the EL, from the coping top EL down to the lowest side bar. + const side = unique.filter((c) => c.group === 'side' && c.x < 0); + const sideLevels = [...new Set(side.map((c) => Math.round(Ymm(c.y))))].sort((a, b) => a - b); + const startY = copingTopEl !== undefined ? Ymm(copingTopEl) : minY; + const endY = sideLevels.length ? sideLevels[sideLevels.length - 1] : maxY; + const sideChain = chainDims(sideLevels, startY, endY); + const dimSide = minX - 700; + for (const d of sideChain) { + parts.push(`${d.text}`); + } + + // 3 (bubbles): one bubble per bar group, with a leader to the centre of the group's crossings. + const groupLabels: { label: string; names: string[] }[] = [ + { label: 'C1–6', names: ['C1', 'C2', 'C3', 'C4', 'C5', 'C6'] }, + { label: 'C12·13', names: ['C12', 'C13'] }, + { label: 'C9', names: ['C9'] }, + { label: 'C10', names: ['C10'] }, + { label: 'C11', names: ['C11'] }, + { label: 'C7·8', names: ['C7', 'C8'] }, + { label: 'C14', names: ['C14'] }, + { label: 'C15', names: ['C15'] }, + { label: 'C16', names: ['C16'] }, + ]; + for (const g of groupLabels) { + const hit = unique.filter((c) => g.names.includes(c.name)); + if (hit.length === 0) continue; + const gx = hit.reduce((s, c) => s + c.x * 1000, 0) / hit.length; + const gy = hit.reduce((s, c) => s + Ymm(c.y), 0) / hit.length; + const cy = gy < (minY + maxY) / 2 ? minY - 1500 : maxY + 1100; + const cx = gx < 0 ? minX - 1400 : maxX + 1400; + const bx = gy < (minY + maxY) / 2 ? gx : cx; + const by = gy < (minY + maxY) / 2 ? cy : gy; + parts.push(``); + parts.push(`${g.label}`); + } + const vx = Math.min(minX, dimSide - 900) - 1500; + const vy = minY - 2600; + const vw = Math.max(maxX, 0) - vx + 1800; + const vh = maxY - vy + 1900; + return `${parts.join('')}`; +} + +function copyGroup(name: string): keyof typeof COLOR { + return copingBarGroup(name); } diff --git a/tests/coping-rebar-svg.test.ts b/tests/coping-rebar-svg.test.ts index a0e7e1d..547a302 100644 --- a/tests/coping-rebar-svg.test.ts +++ b/tests/coping-rebar-svg.test.ts @@ -34,11 +34,12 @@ describe('코핑 배근 정면 · 단면 (승인 2026-10-26)', () => { }); describe('단면 · 폐합 글자 겹침 (승인 2026-10-26)', () => { - it('writes each bar name once, and each spot once', () => { - const svg = copingSectionSvg(scene.records, 6); - const labels = (svg.match(/]*>(C\d+) s.replace(/.*>/, ''))); - expect(labels).toBe(names.size); + it('writes each bubble label once', () => { + const svg = copingSectionSvg(scene.records, 6, model.copingRefTop, model.copingB * 1000); + // A bubble is drawn for each group that is cut at this station, and only once. + for (const label of ['C1–6', 'C12·13', 'C10', 'C7·8', 'C14', 'C16']) { + expect(svg.split(`>${label}<`).length - 1).toBe(1); + } }); });