feat(rebar): 단면·폐합 section with the top-row and side-row chains and bar-group bubbles
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
41c1ecd340
commit
d5bf02e36b
3 files changed
+77
-19
No files matched your search
@@ -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<string>();
|
||||
const named = new Set<string>();
|
||||
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 = `<circle cx="${c.x * 1000}" cy="${(top - c.y) * 1000}" r="${c.group === 'step' ? 22 : 30}" fill="${COLOR[c.group]}"></circle>`;
|
||||
if (named.has(c.name)) return dot;
|
||||
named.add(c.name);
|
||||
return `${dot}<text x="${c.x * 1000 + 60}" y="${(top - c.y) * 1000 - 60}" font-size="120" fill="#18212B">${c.name}</text>`;
|
||||
});
|
||||
return `<svg viewBox="${minX - 800} -800 ${maxX - minX + 1600} ${(top - Math.min(...els)) * 1000 + 1600}" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="코핑 배근 단면 · 폐합 구성 at s = ${s0} m">${parts.join('')}</svg>`;
|
||||
const minY = Math.min(...ys, 0);
|
||||
const maxY = Math.max(...ys, 0);
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const c of unique) {
|
||||
parts.push(`<circle cx="${c.x * 1000}" cy="${Ymm(c.y)}" r="${c.group === 'step' ? 22 : 30}" fill="${COLOR[c.group]}"></circle>`);
|
||||
}
|
||||
|
||||
// 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(`<line x1="${d.from}" y1="${dimTop}" x2="${d.to}" y2="${dimTop}" stroke="#18212B" stroke-width="8"></line><text x="${(d.from + d.to) / 2}" y="${dimTop - 90}" text-anchor="middle" font-size="160" fill="#18212B">${d.text}</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(`<line x1="${dimSide}" y1="${d.from}" x2="${dimSide}" y2="${d.to}" stroke="#18212B" stroke-width="8"></line><text x="${dimSide - 90}" y="${(d.from + d.to) / 2}" text-anchor="end" font-size="160" fill="#18212B" dominant-baseline="middle">${d.text}</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(`<line x1="${bx}" y1="${by}" x2="${gx}" y2="${gy}" stroke="#55606E" stroke-width="6"></line>`);
|
||||
parts.push(`<circle cx="${bx}" cy="${by}" r="200" fill="#FFFFFF" stroke="${COLOR[copyGroup(g.names[0])]}" stroke-width="10"></circle><text x="${bx}" y="${by + 70}" text-anchor="middle" font-size="170" fill="#18212B">${g.label}</text>`);
|
||||
}
|
||||
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 `<svg viewBox="${vx} ${vy} ${vw} ${vh}" width="100%" style="max-width: 600px; height: auto" role="img" aria-label="코핑 배근 단면 · 폐합 구성 at s = ${s0} m">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
function copyGroup(name: string): keyof typeof COLOR {
|
||||
return copingBarGroup(name);
|
||||
}
|
||||
Reference in new issue
Block a user