- The front block's S1 layout goes on the left and the back block's on the right, so the text no longer falls into the gap between the two blocks. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
91 lines
5.7 KiB
TypeScript
91 lines
5.7 KiB
TypeScript
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, side: 1 | -1) => {
|
|
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>`);
|
|
}
|
|
}
|
|
// Dimensions: the S2 layout along the 교축 (below the block), the S1 layout along the 교각축 (beside it), the block width.
|
|
const layout = (count: number, spacing: number, firstEnd: number) => `${firstEnd.toFixed(1)} · ${count - 1}@${spacing}=${(count - 1) * spacing} · ${firstEnd.toFixed(1)}`;
|
|
const a2d = barsAcross(wC * 1000, s.spacingS2, s);
|
|
const a1d = barsAcross(wA * 1000, s.spacingS1, s);
|
|
const [bx, by] = P(wA / 2 + 0.22, 0);
|
|
parts.push(`<text x="${bx}" y="${by}" text-anchor="middle" font-size="80" fill="#0F5C66">${layout(a2d.count, a2d.spacing, a2d.firstEnd)}</text>`);
|
|
// The S1 layout goes on the outer side of the block (the front block on the left, the back block on the right),
|
|
// so it never falls into the gap between the two blocks.
|
|
const [rx, ry] = P(0, side * (wC / 2 + 0.22));
|
|
// The S1 layout runs along the 교각축 (vertical in the plan), so its text is turned to read along that dimension line.
|
|
parts.push(`<text x="${rx}" y="${ry}" transform="rotate(-90 ${rx} ${ry})" text-anchor="middle" font-size="80" fill="#B4531F">${layout(a1d.count, a1d.spacing, a1d.firstEnd)}</text>`);
|
|
const [wx, wy] = P(-wA / 2 - 0.2, 0);
|
|
parts.push(`<text x="${wx}" y="${wy}" text-anchor="end" font-size="90" fill="#18212B">${Math.round(wC * 1000)}</text>`);
|
|
return P;
|
|
};
|
|
|
|
if (ib) {
|
|
const P = drawBlock(ib.s, ib.n, ib.rowSkewDeg, INTEGRATED_BLOCK.depth / 1000, INTEGRATED_BLOCK.length / 1000, 1);
|
|
// 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 {
|
|
drawBlock(f.s, f.n, f.rowSkewDeg, 1.2, 1.2, -1);
|
|
drawBlock(k.s, k.n, k.rowSkewDeg, 1.2, 1.2, 1);
|
|
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>`;
|
|
}
|