feat(abut-rebar): 교대 접속슬래브 배근 삽도 (평면 · 단면)
- 평면: 왼쪽 절반 하면 (R3 · R4 · R8) · 오른쪽 절반 상면 (R1 · R2), 모따기 모서리 R7, 교대축 · 길이 배치 치수 - 단면 (길이 방향, 폭 가운데): 브라켓 위 평탄 + 경사, 층 순서, R8은 R1 · R3 사이 - verify/dump: 접속슬래브 삽도 덤프 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ae6cf0945
commit
e9d79957c8
3 files changed
+173
No files matched your search
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 교대 접속슬래브 배근 삽도 (2026-11-05, 시안 ③ 접속슬래브 카드, 교대접속슬래브철근.md §9): 평면 (왼쪽 절반 하면 · 오른쪽 절반 상면,
|
||||
* 삽도와 같음), 단면 (길이 방향, 폭 가운데). 순수 문자열 함수, 철근은 배근 규칙의 경로(slab.ts paths, 교대 s · t · z)에서 그린다.
|
||||
* 색: 길이 방향 주철근 #0F5C66, 교대축 방향 #B4531F, R7 #C0501A, R8 #6B3FA0.
|
||||
*/
|
||||
import type { AbutSlabGeom, AbutSlabRebar, SlabBarPath, SlabMark } from '../core/abut/rebar/slab';
|
||||
import { balloon, CONC, MUTED, NO_PICK, opacity, text, type AbutRebarPick } from './abut-footing-rebar-svg';
|
||||
|
||||
export const ABUT_SLAB_COLORS = { main: '#0F5C66', dist: '#B4531F', corner: '#C0501A', shear: '#6B3FA0' } as const;
|
||||
const C = ABUT_SLAB_COLORS;
|
||||
const COLOR: Record<SlabMark, string> = { R1: C.main, R3: C.main, R2: C.dist, R4: C.dist, R7: C.corner, R8: C.shear };
|
||||
|
||||
/** u (0 측면에서 교대축을 따라) and t' (흉벽 쪽 끝에서) of a plan point. */
|
||||
const frame = (g: AbutSlabGeom) => ({
|
||||
u: (s: number, t: number) => s - (g.side0.s + g.side0.k * (t - g.near)),
|
||||
tp: (t: number) => g.near - t,
|
||||
});
|
||||
|
||||
/** 평면: 가로 u (교대축), 세로 t' (위 = 흉벽 쪽 끝). 왼쪽 절반은 하면 (R3 · R4 · R8 · 하면 R7), 오른쪽 절반은 상면 (R1 · R2 · 상면 R7). */
|
||||
export function abutSlabPlanSvg(g: AbutSlabGeom, r: AbutSlabRebar, pick: AbutRebarPick = NO_PICK): string {
|
||||
const f = frame(g);
|
||||
const k = 0.055;
|
||||
const X = (u: number) => 50 + u * k;
|
||||
const Y = (tp: number) => 60 + tp * k;
|
||||
const half = g.B / 2;
|
||||
const parts: string[] = [];
|
||||
const P = (u: number, tp: number) => `${X(u).toFixed(1)},${Y(tp).toFixed(1)}`;
|
||||
const outline = [P(g.chamfer0, 0), P(g.B - g.chamferL, 0), P(g.B, g.chamferL), P(g.B, g.Ls), P(0, g.Ls), P(0, g.chamfer0)];
|
||||
parts.push(`<polygon points="${outline.join(' ')}" fill="${CONC}" stroke="${MUTED}"/>`);
|
||||
parts.push(`<line x1="${X(half)}" y1="${Y(0) - 10}" x2="${X(half)}" y2="${Y(g.Ls) + 10}" stroke="${MUTED}" stroke-dasharray="6 4"/>`);
|
||||
parts.push(text(X(half / 2), Y(g.Ls) + 24, '하면 (R3 · R4 · R8)', 'middle', MUTED, 11), text(X(half * 1.5), Y(g.Ls) + 24, '상면 (R1 · R2)', 'middle', MUTED, 11));
|
||||
parts.push(text(X(half), Y(0) - 14, `흉벽 쪽 끝 · ${r.across.text}`, 'middle', MUTED, 10));
|
||||
const seg = (mark: SlabMark, a: [number, number], b: [number, number], w: number) =>
|
||||
`<line x1="${X(a[0]).toFixed(1)}" y1="${Y(a[1]).toFixed(1)}" x2="${X(b[0]).toFixed(1)}" y2="${Y(b[1]).toFixed(1)}" stroke="${COLOR[mark]}" stroke-width="${w}" opacity="${opacity(pick, [mark])}"/>`;
|
||||
const uvOf = (p: SlabBarPath) => p.pts.map(([s, t]) => [f.u(s, t), f.tp(t)] as [number, number]);
|
||||
for (const p of r.paths) {
|
||||
const uv = uvOf(p);
|
||||
const bottom = p.mark === 'R3' || p.mark === 'R4' || p.mark === 'R8';
|
||||
if (p.mark === 'R8') {
|
||||
const [u, tp] = uv[1];
|
||||
if (u < half) parts.push(`<circle cx="${X(u).toFixed(1)}" cy="${Y(tp).toFixed(1)}" r="2.2" fill="${C.shear}" opacity="${opacity(pick, ['R8'])}"/>`);
|
||||
continue;
|
||||
}
|
||||
if (p.mark === 'R7') {
|
||||
// 상 · 하 R7은 평면에서 같은 자리라 그대로 그린다 (모서리마다 한 모양).
|
||||
for (let i = 1; i < uv.length; i++) parts.push(seg('R7', uv[i - 1], uv[i], 1.6));
|
||||
continue;
|
||||
}
|
||||
// 길이 방향 (R1 · R3)은 절반 안의 것만, 교대축 방향 (R2 · R4)은 절반까지 자름.
|
||||
if (p.mark === 'R1' || p.mark === 'R3') {
|
||||
const u = uv[0][0];
|
||||
if (bottom ? u < half : u >= half) for (let i = 1; i < uv.length; i++) parts.push(seg(p.mark, uv[i - 1], uv[i], 0.7));
|
||||
} else {
|
||||
const [a, b] = [uv[0], uv[uv.length - 1]];
|
||||
const lo = a[0] < b[0] ? a : b;
|
||||
const hi = a[0] < b[0] ? b : a;
|
||||
if (bottom) parts.push(seg(p.mark, lo, [Math.min(hi[0], half), hi[1]], 0.7));
|
||||
else parts.push(seg(p.mark, [Math.max(lo[0], half), lo[1]], hi, 0.7));
|
||||
}
|
||||
}
|
||||
parts.push(text(X(g.B) + 8, Y(g.Ls / 2), r.along.text, 'start', MUTED, 10));
|
||||
parts.push(balloon(X(half * 0.25), Y(g.Ls) + 50, 'R3', C.main, pick), balloon(X(half * 0.5), Y(g.Ls) + 50, 'R4', C.dist, pick), balloon(X(half * 0.75), Y(g.Ls) + 50, 'R8', C.shear, pick));
|
||||
parts.push(balloon(X(half * 1.4), Y(g.Ls) + 50, 'R1', C.main, pick), balloon(X(half * 1.7), Y(g.Ls) + 50, 'R2', C.dist, pick), balloon(X(g.B * 0.06), Y(0) + 40, 'R7', C.corner, pick));
|
||||
const W = X(g.B) + 260;
|
||||
const H = Y(g.Ls) + 80;
|
||||
return `<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" style="max-width:${Math.round(W * 1.3)}px" role="img" aria-label="접속슬래브 배근 평면 (왼쪽 하면 · 오른쪽 상면)">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
/** 단면 (길이 방향, 폭 가운데): 가로 t' (왼쪽 흉벽 쪽 끝), 세로 높이. 경사, 층 순서 (R1 · R2 위, R4 · R3 아래), R8. */
|
||||
export function abutSlabSectionSvg(g: AbutSlabGeom, r: AbutSlabRebar, pick: AbutRebarPick = NO_PICK): string {
|
||||
const f = frame(g);
|
||||
const uMid = g.B / 2;
|
||||
const kx = 0.11;
|
||||
const kz = 0.5;
|
||||
const sMid = (t: number) => g.side0.s + g.side0.k * (t - g.near) + uMid;
|
||||
const parapetAt = (sv: number) => g.parapetTop[0] + ((g.parapetTop[1] - g.parapetTop[0]) * sv) / g.L;
|
||||
const zs = g.stations.flatMap((st) => [parapetAt(sMid(st.t)) + st.top, parapetAt(sMid(st.t)) + st.bottom]);
|
||||
const zMax = Math.max(...zs) + 60;
|
||||
const zMin = Math.min(...zs) - 60;
|
||||
const X = (tp: number) => 40 + tp * kx;
|
||||
const Y = (z: number) => 50 + (zMax - z) * kz;
|
||||
const parts: string[] = [];
|
||||
const topPts = g.stations.map((st) => `${X(f.tp(st.t)).toFixed(1)},${Y(parapetAt(sMid(st.t)) + st.top).toFixed(1)}`);
|
||||
const botPts = [...g.stations].reverse().map((st) => `${X(f.tp(st.t)).toFixed(1)},${Y(parapetAt(sMid(st.t)) + st.bottom).toFixed(1)}`);
|
||||
parts.push(`<polygon points="${[...topPts, ...botPts].join(' ')}" fill="${CONC}" stroke="${MUTED}"/>`);
|
||||
// 폭 가운데에 가장 가까운 길이 방향 철근 (R1 · R3), 교대축 방향 철근 (R2 · R4) 점, R8 한 줄.
|
||||
const nearest = (mark: SlabMark) =>
|
||||
r.paths.filter((p) => p.mark === mark).reduce<SlabBarPath | undefined>((a, b) => (!a || Math.abs(f.u(b.pts[0][0], b.pts[0][1]) - uMid) < Math.abs(f.u(a.pts[0][0], a.pts[0][1]) - uMid) ? b : a), undefined);
|
||||
for (const mark of ['R1', 'R3'] as const) {
|
||||
const p = nearest(mark);
|
||||
if (p) parts.push(`<polyline points="${p.pts.map(([, t, z]) => `${X(f.tp(t)).toFixed(1)},${Y(z).toFixed(1)}`).join(' ')}" fill="none" stroke="${C.main}" stroke-width="1.6" opacity="${opacity(pick, [mark])}"/>`);
|
||||
}
|
||||
for (const p of r.paths.filter((q) => q.mark === 'R2' || q.mark === 'R4')) {
|
||||
const t = p.pts[0][1];
|
||||
const a = p.pts[0];
|
||||
const b = p.pts[p.pts.length - 1];
|
||||
const fz = (uMid - f.u(a[0], t)) / (f.u(b[0], t) - f.u(a[0], t));
|
||||
const z = a[2] + fz * (b[2] - a[2]);
|
||||
parts.push(`<circle cx="${X(f.tp(t)).toFixed(1)}" cy="${Y(z).toFixed(1)}" r="1.8" fill="${C.dist}" opacity="${opacity(pick, [p.mark])}"/>`);
|
||||
}
|
||||
// R8: 줄마다 하나, 그린 R1 · R3 선 사이 (슬래브 상면이 교대축을 따라 기울어 실제 R8마다 높이가 조금씩 다름).
|
||||
const zOn = (p: SlabBarPath | undefined, t: number) => {
|
||||
if (!p) return NaN;
|
||||
for (let i = 1; i < p.pts.length; i++) {
|
||||
const [a0, a1] = [p.pts[i - 1], p.pts[i]];
|
||||
if ((t - a0[1]) * (t - a1[1]) <= 0) return a0[2] + ((t - a0[1]) / (a1[1] - a0[1] || 1)) * (a1[2] - a0[2]);
|
||||
}
|
||||
return p.pts[p.pts.length - 1][2];
|
||||
};
|
||||
const r1 = nearest('R1');
|
||||
const r3 = nearest('R3');
|
||||
const rowTs = [...new Set(r.paths.filter((q) => q.mark === 'R8').map((q) => Math.round(q.pts[1][1])))];
|
||||
for (const t of rowTs) {
|
||||
const zt = zOn(r1, t) + 11.1;
|
||||
const zb = zOn(r3, t) - 11.1;
|
||||
parts.push(`<line x1="${X(f.tp(t)).toFixed(1)}" y1="${Y(zb).toFixed(1)}" x2="${X(f.tp(t)).toFixed(1)}" y2="${Y(zt).toFixed(1)}" stroke="${C.shear}" stroke-width="0.9" opacity="${opacity(pick, ['R8'])}"/>`);
|
||||
}
|
||||
parts.push(text(X(0), Y(zMin) + 16, '흉벽 쪽 끝 (브라켓 위)', 'start', MUTED, 10), text(X(g.Ls), Y(zMin) + 16, `먼 끝 · 경사 ${g.gradePct.toFixed(1)} %`, 'end', MUTED, 10));
|
||||
parts.push(text(X(g.Ls / 2), 18, `${g.form === 'buried' ? '매립형' : '노출형'} · 상면 R1 → R2, 하면 R3 → R4 (피복 ${r.settings.cv}) · R8 @${Math.round(r.settings.k8 * (r.settings.sDist ?? r.settings.sL))}`, 'middle', MUTED, 10));
|
||||
parts.push(balloon(X(g.Ls * 0.3), 40, 'R1', C.main, pick), balloon(X(g.Ls * 0.4), 40, 'R2', C.dist, pick), balloon(X(g.Ls * 0.5), 40, 'R8', C.shear, pick), balloon(X(g.Ls * 0.6), 40, 'R4', C.dist, pick), balloon(X(g.Ls * 0.7), 40, 'R3', C.main, pick));
|
||||
const W = X(g.Ls) + 40;
|
||||
const H = Y(zMin) + 30;
|
||||
return `<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" style="max-width:${Math.round(W * 1.2)}px" role="img" aria-label="접속슬래브 배근 단면 (길이 방향)">${parts.join('')}</svg>`;
|
||||
}
|
||||
Reference in new issue
Block a user