/**
* 교대 저판 배근 삽도 (2026-11-04, 시안 ③ 확대기초 카드, 교대확대기초철근.md §10): 교대축 직각 단면 (폭 W), 교대축 단면 (길이 L),
* 상면 평면. 순수 문자열 함수. 번호 풍선(data-pick)을 누르면 그 철근만 강조한다; 선택한 번호는 `pick`(철근표 번호 집합)으로, 선택만 보기는 `only`로 받는다.
* 색은 역할 기준: 주철근 B1–B4 #0F5C66, 보조 B5–B8 #B4531F, 측면 B10 #2E7D32 점선, 전단 B9 #6B3FA0.
*/
import type { AbutFootingGeom, AbutFootingRebar } from '../core/abut/rebar/footing';
export const ABUT_FOOTING_COLORS = { main: '#0F5C66', dist: '#B4531F', side: '#2E7D32', shear: '#6B3FA0' } as const;
const INK = '#18212b';
const MUTED = '#55606e';
const CONC = '#eef1f4';
const WALL = '#d9dee4';
const text = (x: number, y: number, s: string, anchor = 'middle', fill = INK, size = 11) =>
`${s}`;
/** Selected bars (철근표 번호) and 선택만 보기. An empty selection shows every bar. */
export type AbutRebarPick = { marks: ReadonlySet; only: boolean };
const NO_PICK: AbutRebarPick = { marks: new Set(), only: false };
/** Opacity of one bar group: dimmed (or hidden with 선택만 보기) when other bars are selected. */
function opacity(pick: AbutRebarPick, marks: string[]): number {
if (pick.marks.size === 0) return 1;
return marks.some((m) => pick.marks.has(m)) ? 1 : pick.only ? 0 : 0.15;
}
/** A clickable 번호 풍선. */
function balloon(x: number, y: number, mark: string, color: string, pick: AbutRebarPick): string {
// The B10 풍선 stands for B10-1 and B10-2.
const on = [...pick.marks].some((m) => m === mark || (mark === 'B10' && m.startsWith('B10')));
const r = mark.length > 3 ? 15 : 12;
return `${text(x, y + 4, mark, 'middle', on ? '#fff' : color, 10)}`;
}
/** 교대축 직각 단면 (폭 W): 왼쪽 배면(뒷굽) → 오른쪽 전면(앞굽). B1–B4 선, B5–B8 점, B10-1 점, B9 연직선. */
export function abutFootingSectionWSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: AbutRebarPick = NO_PICK): string {
const k = 0.12;
const x0 = 60;
const y0 = 46;
const X = (x: number) => x0 + x * k;
const Y = (z: number) => y0 + (g.H - z) * k;
const c = r.settings.cSide;
const C = ABUT_FOOTING_COLORS;
const parts: string[] = [];
parts.push(``);
parts.push(text((X(g.xb) + X(g.xf)) / 2, y0 - 10, `벽체 ${Math.round(g.xf - g.xb)} (상면 B5·B7 없음)`, 'middle', MUTED, 10));
parts.push(``);
const line = (pts: [number, number][], color: string, marks: string[], w = 1.8, dash = '') =>
``;
const { B13, B24, B57, B68 } = r.z;
// B9 (behind the others): vertical lines at the row positions.
for (const row of r.shear.rows) parts.push(line([[row.x, B24], [row.x, B13]], C.shear, ['B9'], 1.2));
// B1 ⊓ (slightly inside so the legs show next to B2's), B2 ⊔, B3 to the 벽체 전면 − c, B4 straight.
const lx = c + 6;
parts.push(line([[lx, B24], [lx, B13], [g.W - lx, B13], [g.W - lx, B24]], C.main, ['B1']));
parts.push(line([[c, B13], [c, B24], [g.W - c, B24], [g.W - c, B13]], C.main, ['B2']));
parts.push(line([[c, B13 + 14], [g.xf - c, B13 + 14]], C.main, ['B3'], 1.6, '6 3'));
parts.push(line([[c, B24 - 14], [g.W - c, B24 - 14]], C.main, ['B4'], 1.6, '6 3'));
// B5–B8 dots.
const dot = (x: number, z: number, color: string, mark: string) =>
``;
r.dist.positions.forEach((x, i) => {
if (r.dist.topKept[i]) parts.push(dot(x + (i % 2 === 0 ? r.sideShift.B5 : 0), B57, C.dist, i % 2 === 0 ? 'B5' : 'B7'));
parts.push(dot(x, B68, C.dist, i % 2 === 0 ? 'B6' : 'B8'));
});
// B10-1 dots on both faces.
for (const z of r.side.levels) {
const zz = z + 19.1;
parts.push(dot(r.side.depth1, zz, C.side, 'B10-1'), dot(g.W - r.side.depth1, zz, C.side, 'B10-1'));
}
// 치수 문자열.
parts.push(text(X(g.W / 2), Y(0) + 18, r.dist.bottomText + ' (하면)', 'middle', INK, 10));
parts.push(text(X(g.W / 2), Y(g.H) - 32, r.dist.topText + ' (상면)', 'middle', INK, 10));
parts.push(text(X(0) - 6, Y(g.H / 2), r.side.text, 'end', MUTED, 10));
// 번호 풍선.
const by = Y(g.H) - 2;
parts.push(balloon(X(g.xb / 2), Y(B13) + 26, 'B1', C.main, pick), balloon(X(g.xb / 2) + 30, Y(B13) + 26, 'B3', C.main, pick));
parts.push(balloon(X(g.xb / 2), Y(B24) - 26, 'B2', C.main, pick), balloon(X(g.xb / 2) + 30, Y(B24) - 26, 'B4', C.main, pick));
parts.push(balloon(X(g.xf + (g.W - g.xf) / 2) - 15, Y(B57) + 30, 'B5', C.dist, pick), balloon(X(g.xf + (g.W - g.xf) / 2) + 15, Y(B57) + 30, 'B7', C.dist, pick));
parts.push(balloon(X(g.xf + (g.W - g.xf) / 2) - 15, Y(B68) - 30, 'B6', C.dist, pick), balloon(X(g.xf + (g.W - g.xf) / 2) + 15, Y(B68) - 30, 'B8', C.dist, pick));
if (r.shear.rows.length) parts.push(balloon(X(r.shear.rows[0].x) + 22, Y(g.H / 2), 'B9', C.shear, pick));
parts.push(balloon(X(g.W) + 22, Y(r.side.levels[1] ?? g.H / 2), 'B10', C.side, pick));
parts.push(text(X(0), by - 30, '← 배면 (뒷굽)', 'start', MUTED, 10), text(X(g.W), by - 30, '전면 (앞굽) →', 'end', MUTED, 10));
const W = X(g.W) + 50;
const H = Y(0) + 30;
return ``;
}
/** 교대축 단면 (길이 L, 가로 축척 0.4배): B5–B8 선(이음 위치), B1–B4 점, B10-2 점. 왼쪽 IP3 쪽. */
export function abutFootingSectionLSvg(g: AbutFootingGeom, r: AbutFootingRebar, pick: AbutRebarPick = NO_PICK): string {
const kx = 0.045;
const kz = 0.12;
const x0 = 50;
const y0 = 30;
const X = (s: number) => x0 + s * kx;
const Y = (z: number) => y0 + (g.H - z) * kz;
const C = ABUT_FOOTING_COLORS;
const parts: string[] = [];
parts.push(``);
const e = r.settings.cSide / g.angle.sin;
const line = (pts: [number, number][], color: string, marks: string[], w = 1.8, dash = '') =>
``;
const { B13, B24, B57, B68 } = r.z;
parts.push(line([[e + 4, B68], [e + 4, B57], [g.L - e - 4, B57], [g.L - e - 4, B68]], C.dist, ['B5']));
parts.push(line([[e, B57], [e, B68], [g.L - e, B68], [g.L - e, B57]], C.dist, ['B6']));
parts.push(line([[e, B57 - 14], [g.L - e, B57 - 14]], C.dist, ['B7'], 1.4, '6 3'));
parts.push(line([[e, B68 + 14], [g.L - e, B68 + 14]], C.dist, ['B8'], 1.4, '6 3'));
// 이음 (B5 · B6): the two splice centres (± l_s around the middle) as ticks.
const lap = r.bars.find((b) => b.mark === 'B5')?.splice?.lap;
if (lap) {
for (const sc of [g.L / 2 - lap, g.L / 2 + lap]) {
parts.push(``);
}
parts.push(text(X(g.L / 2), Y(B57) - 10, `B5·B6 이음 l_s ${lap} (± l_s 엇갈림)`, 'middle', C.dist, 10));
}
const dot = (s: number, z: number, color: string, mark: string) =>
``;
r.main.positions.forEach((s, i) => {
const one = i % 2 === 0;
parts.push(dot(one ? s + r.sideShift.B1 : s, B13, C.main, one ? 'B1' : 'B3'));
parts.push(dot(s, B24, C.main, one ? 'B2' : 'B4'));
});
const d2 = r.side.depth2 / g.angle.sin;
for (const z of r.side.levels) parts.push(dot(d2, z, C.side, 'B10-2'), dot(g.L - d2, z, C.side, 'B10-2'));
parts.push(text(X(g.L / 2), Y(0) + 18, r.main.text, 'middle', INK, 10));
parts.push(balloon(X(g.L * 0.12), Y(B13) - 16, 'B1', C.main, pick), balloon(X(g.L * 0.12) + 28, Y(B13) - 16, 'B3', C.main, pick));
parts.push(balloon(X(g.L * 0.88), Y(B57) + 30, 'B5', C.dist, pick), balloon(X(g.L * 0.88) + 28, Y(B57) + 30, 'B7', C.dist, pick));
parts.push(balloon(X(g.L * 0.3), Y(B68) - 30, 'B6', C.dist, pick), balloon(X(g.L * 0.3) + 28, Y(B68) - 30, 'B8', C.dist, pick));
parts.push(balloon(X(g.L * 0.12), Y(B24) + 20, 'B2', C.main, pick), balloon(X(g.L * 0.12) + 28, Y(B24) + 20, 'B4', C.main, pick));
parts.push(balloon(X(g.L) + 22, Y(g.H / 2), 'B10', C.side, pick));
const W = X(g.L) + 50;
const H = Y(0) + 30;
return ``;
}
/**
* 상면 평면: skew 평행사변형, 상면 B1·B3 (옵션에 따라 skew 선과 나란히 / 교대축 직각, 옵션 ② B1-S), B5·B7, B9, 벽체 밑 띠.
* `skewK` = 날개벽 방향의 s / t 비 (장면의 abutSkewK): s(x) = s + skewK · x. 위 = 전면, 왼쪽 = IP3 끝 (s = L).
*/
export function abutFootingRebarPlanSvg(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, pick: AbutRebarPick = NO_PICK): string {
const k = 0.045;
const shiftMax = Math.abs(skewK) * g.W;
const sMin = Math.min(0, skewK * g.W);
const x0 = 30;
const y0 = 24;
const X = (s: number) => x0 + (g.L + shiftMax + sMin - s) * k; // s = L (IP3) on the left
const Y = (x: number) => y0 + (g.W - x) * k; // 전면 (x = W) on top
const P = (s: number, x: number) => `${X(s + skewK * x).toFixed(1)},${Y(x).toFixed(1)}`;
// Option ② positions start after the end face that leans out (core: start = c + W·|cot θ|); with the faces leaning the
// other way (skewK < 0) that side is at x = W, so the square bars sit W·|cot θ| further toward s = 0 in this frame.
const SQ = (s: number) => (skewK >= 0 ? s : s - shiftMax);
const C = ABUT_FOOTING_COLORS;
const c = r.settings.cSide;
const parts: string[] = [];
parts.push(``);
parts.push(``);
const seg = (a: string, b: string, color: string, mark: string, w = 1, dash = '') =>
``;
// B5 · B7: along the axis at the kept top positions, cut square at the ends (끝 피복 c / sin θ).
const e = c / g.angle.sin;
r.dist.positions.forEach((x, i) => {
if (!r.dist.topKept[i]) return;
parts.push(seg(P(e, x), P(g.L - e, x), C.dist, i % 2 === 0 ? 'B5' : 'B7', 0.8));
});
// B1 · B3: option ① along the skew line from the end-face grid; option ② square to the axis.
r.main.positions.forEach((s, i) => {
const one = i % 2 === 0;
const xEnd = one ? g.W - c : g.xf - c;
if (r.dir === 'skew') parts.push(seg(P(s, c), P(s, xEnd), C.main, one ? 'B1' : 'B3', 0.8, one ? '' : '4 2'));
else parts.push(seg(`${X(SQ(s)).toFixed(1)},${Y(c).toFixed(1)}`, `${X(SQ(s)).toFixed(1)},${Y(xEnd).toFixed(1)}`, C.main, one ? 'B1' : 'B3', 0.8, one ? '' : '4 2'));
});
if (r.skewEnd) {
for (const off of r.skewEnd.offsets) {
parts.push(seg(P(c + off, c), P(c + off, g.W - c), C.main, 'B1-S', 1.4));
parts.push(seg(P(g.L - c - off, c), P(g.L - c - off, g.W - c), C.main, 'B1-S', 1.4));
}
}
// B9 dots (rows).
for (const row of r.shear.rows) {
for (const s of row.s) {
const at = r.dir === 'skew' ? P(s, row.x) : `${X(SQ(s)).toFixed(1)},${Y(row.x).toFixed(1)}`;
const [cx, cy] = at.split(',');
parts.push(``);
}
}
parts.push(text(X(g.L / 2 + skewK * (g.xb + g.xf) / 2), Y((g.xb + g.xf) / 2) + 4, '벽체 밑 (상면 B5·B7 없음, B9 없음)', 'middle', MUTED, 10));
parts.push(text(X(g.L / 2 + skewK * g.W), Y(g.W) - 6, '전면 (앞굽 끝)', 'middle', MUTED, 10));
parts.push(text(X(g.L / 2), Y(0) + 14, '배면 (뒷굽 끝)', 'middle', MUTED, 10));
parts.push(text(X(g.L) - 4, Y(g.W / 2), 'IP3', 'end', MUTED, 10), text(X(0) + 4, Y(g.W / 2), 'IP1', 'start', MUTED, 10));
parts.push(balloon(X(g.L * 0.75 + skewK * g.W * 0.7), Y(g.W * 0.7), 'B1', C.main, pick), balloon(X(g.L * 0.75 + skewK * g.W * 0.7) + 28, Y(g.W * 0.7), 'B3', C.main, pick));
parts.push(balloon(X(g.L * 0.4 + skewK * g.W * 0.9), Y(g.W * 0.92), 'B5', C.dist, pick), balloon(X(g.L * 0.4 + skewK * g.W * 0.9) + 28, Y(g.W * 0.92), 'B7', C.dist, pick));
parts.push(balloon(X(g.L * 0.25 + skewK * g.W * 0.1), Y(g.W * 0.1), 'B9', C.shear, pick));
if (r.skewEnd) parts.push(balloon(X(c + skewK * g.W * 0.5) - 26, Y(g.W * 0.5), 'B1-S', C.main, pick));
const W = x0 * 2 + (g.L + shiftMax) * k;
const H = Y(0) + 26;
return ``;
}