- 경사 선택 삭제: 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (노트 표기). 예전 파일의 slope 값은 읽고 무시 - 단면 + 접합 상세: 흉벽 쪽과 먼 끝만 그리고 중간 생략, 부속재 · 치수(7000 · 450 · 850 · 295 · 헌치) · EL을 한 그림에 - 흉벽 높이 (브라켓 상면 ~ 흉벽 상단) 입력 = 벽체·흉벽 카드 브라켓 높이와 같은 값 - 자동 값: 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (295) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
185 lines
11 KiB
TypeScript
185 lines
11 KiB
TypeScript
/**
|
||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이)과 단면 + 접합 상세(매립형 / 노출형, 중간 생략).
|
||
* 순수 문자열 함수. 생성본 값으로 그리고 가로·세로 축척이 같다(단면은 파단선으로 중간을 생략). 접합 부속재는 표시용이다.
|
||
*/
|
||
import type { AbutPlanOutline } from './abut-scene';
|
||
import { slabLevelAt, type SlabForm, type SlabStation } from '../core/abut/slab';
|
||
import { dimLine } from './abut-wing-svg';
|
||
|
||
const INK = '#18212b';
|
||
const MUTED = '#55606e';
|
||
const WING = '#d6bb8c';
|
||
const WALL = '#b7c7d3';
|
||
const SLAB = '#f3eee4';
|
||
const SLAB_EDGE = '#b49a6b';
|
||
const text = (x: number, y: number, s: string, anchor = 'start', fill = INK) =>
|
||
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" font-size="11" fill="${fill}" font-family="JetBrains Mono, monospace">${s}</text>`;
|
||
const poly = (pts: [number, number][], style: string) => `<polygon points="${pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ')}" ${style}/>`;
|
||
const line = (x1: number, y1: number, x2: number, y2: number, style = `stroke="${MUTED}"`) =>
|
||
`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" ${style}/>`;
|
||
const el = (v: number) => `EL.${v.toFixed(3)}`;
|
||
|
||
export interface AbutSlabPlanDrawing {
|
||
outline: AbutPlanOutline;
|
||
/** 좌 = ① 쪽 끝 (sEnd); 날개벽 평면과 같은 좌우. */
|
||
leftSEnd: number;
|
||
widthM: number;
|
||
lengthMm: number;
|
||
/** 측별 모따기 (mm, 0 = 없음). */
|
||
chamferMm: { left: number; right: number };
|
||
}
|
||
|
||
/** 평면: 벽체, 날개벽, 접속슬래브(모따기), 폭 · 길이 치수. 왼쪽이 ① 쪽(좌), 위가 전면. */
|
||
export function abutSlabPlanSvg(d: AbutSlabPlanDrawing): string {
|
||
const pts = [...d.outline.wall, ...d.outline.slab, ...d.outline.wings.flatMap((w) => w.poly)];
|
||
const flip = d.leftSEnd !== 0;
|
||
const xs = pts.map(([s]) => (flip ? -s : s));
|
||
const ts = pts.map(([, t]) => t);
|
||
const k = 30;
|
||
const xMin = Math.min(...xs) - 1.6;
|
||
const tMax = Math.max(...ts) + 0.4;
|
||
const X = (s: number) => 20 + ((flip ? -s : s) - xMin) * k;
|
||
const Y = (t: number) => 18 + (tMax - t) * k;
|
||
const W = 40 + (Math.max(...xs) - xMin + 0.6) * k;
|
||
const tMin = Math.min(...d.outline.slab.map(([, t]) => t));
|
||
const H = Y(tMin) + 46;
|
||
const map = (p: [number, number][]) => p.map(([s, t]) => [X(s), Y(t)] as [number, number]);
|
||
const slab = d.outline.slab;
|
||
const parts = [
|
||
poly(map(d.outline.wall), `fill="${WALL}" stroke="${MUTED}"`),
|
||
...d.outline.wings.map((w) => poly(map(w.poly), `fill="${WING}" stroke="#9c8763"`)),
|
||
poly(map(slab), `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`),
|
||
];
|
||
// 폭: the far edge (the last two corners), below it. 길이: the left side, from the near edge to the far edge.
|
||
const farA = slab[slab.length - 1];
|
||
const farB = slab[slab.length - 2];
|
||
const [fl, fr] = X(farA[0]) < X(farB[0]) ? [farA, farB] : [farB, farA];
|
||
parts.push(dimLine([X(fl[0]), Y(fl[1])], [X(fr[0]), Y(fr[1])], Y(tMin) + 16, 'h', `폭 ${d.widthM.toFixed(3)} m`, true));
|
||
const tNear = Math.max(...slab.map(([, t]) => t));
|
||
const xLeft = Math.min(...slab.map(([s]) => X(s)));
|
||
const nearLeft = slab.filter(([, t]) => Math.abs(t - tNear) < 1e-9).map(([s]) => X(s)).sort((a, b) => a - b)[0];
|
||
parts.push(dimLine([nearLeft, Y(tNear)], [X(fl[0]), Y(fl[1])], xLeft - 12, 'v', `${d.lengthMm}`));
|
||
const c = slab.reduce((a, [s, t]) => [a[0] + s / slab.length, a[1] + t / slab.length], [0, 0]);
|
||
parts.push(text(X(c[0]), Y(c[1]), '접속슬래브', 'middle'));
|
||
for (const w of d.outline.wings) {
|
||
const mm = w.side === 'left' ? d.chamferMm.left : d.chamferMm.right;
|
||
if (!(mm > 0)) continue;
|
||
const x = X(w.sEnd) + (X(c[0]) > X(w.sEnd) ? 1 : -1) * 34;
|
||
parts.push(text(x, Y(tNear) + 26, `모따기 ${mm} × ${mm}`, X(c[0]) > X(w.sEnd) ? 'start' : 'end', MUTED));
|
||
}
|
||
parts.push(text((X(0) + X(Math.max(...d.outline.wall.map(([s]) => s)))) / 2, 12, '흉벽 · 벽체 (위 = 전면)', 'middle', MUTED));
|
||
return [
|
||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="접속슬래브 평면: 폭 ${d.widthM.toFixed(3)} m, 길이 ${d.lengthMm}">`,
|
||
...parts,
|
||
'</svg>',
|
||
].join('');
|
||
}
|
||
|
||
export interface AbutSlabSectionDrawing {
|
||
form: SlabForm;
|
||
/** "하향 4 %" / "종단경사 +1.91 %". */
|
||
slopeLabel: string;
|
||
/** 단면 (t, 흉벽 상단에서 잰 상면 · 하면), core/abut/slab.ts slabSection. */
|
||
section: SlabStation[];
|
||
/** 이 단면 위치의 흉벽 상단 EL (m). */
|
||
parapetTopEl: number;
|
||
bracketHeightM: number;
|
||
parapetBackM: number;
|
||
wallBackM: number;
|
||
lengthMm: number;
|
||
thicknessMm: number;
|
||
parapetGapMm: number;
|
||
haunchLengthMm: number;
|
||
}
|
||
|
||
/**
|
||
* 단면 + 접합 상세 (2026-11-04, 그림 합침): 교대축 직각, 왼쪽이 배면. 흉벽 쪽(헌치 끝 너머까지)과 먼 끝 0.8 m만 같은 축척으로
|
||
* 그리고 그 사이는 파단선으로 생략한다. 간격(5)과 고무판 두께는 보이도록 키워 그리고, 부속재(충진재 · 스티로폼 · 고무판 ·
|
||
* 타르페이퍼 · 다웰 · 강관)는 표시용이다. 치수: 7000 (중간 생략), 450, 흉벽 높이(브라켓 상면 ~ 흉벽 상단), 헌치, 브라켓 얹힘 길이.
|
||
*/
|
||
export function abutSlabSectionSvg(d: AbutSlabSectionDrawing): string {
|
||
const k = 150; // px per m in both zones
|
||
const st = d.section;
|
||
const t0 = st[0].t;
|
||
const t1 = st[st.length - 1].t;
|
||
const bH = d.bracketHeightM;
|
||
const wb = d.wallBackM;
|
||
const pb = d.parapetBackM;
|
||
const he = wb - d.haunchLengthMm / 1000;
|
||
const farLen = 0.8;
|
||
const tz = Math.max(d.form === 'exposed' ? he - 0.5 : wb - 1.5, t1 + farLen); // left end of the near zone
|
||
const breakW = 34;
|
||
const xL = 70;
|
||
const xNear0 = xL + farLen * k + breakW;
|
||
const X = (t: number) => (t <= t1 + farLen + 1e-9 ? xL + (t - t1) * k : xNear0 + (t - tz) * k);
|
||
const gapPx = 4; // 간격 drawn 4 px wide whatever its size
|
||
const Xs = (t: number) => X(t) - (t >= t0 - 1e-9 ? gapPx - (d.parapetGapMm / 1000) * k : 0);
|
||
const low = -bH - 0.75;
|
||
const hi = Math.max(0, ...st.map((p) => p.top)) + 0.15;
|
||
const Y = (y: number) => 26 + (hi - y) * k;
|
||
const lv = (t: number) => slabLevelAt(st, t);
|
||
// Slab pieces: far (t1 … t1 + 0.8) and near (tz … t0), each closed by a zigzag at the cut.
|
||
const zig = (x: number, top: number, bottom: number, dir: 1 | -1): [number, number][] => {
|
||
const h = bottom - top;
|
||
return [[x, top - 4], [x, top + h * 0.3], [x + dir * 6, top + h * 0.45], [x - dir * 6, top + h * 0.6], [x, top + h * 0.72], [x, bottom + 4]];
|
||
};
|
||
const piece = (from: number, to: number, cutLeft: boolean) => {
|
||
const ts = [from, ...st.map((p) => p.t).filter((t) => t > from + 1e-9 && t < to - 1e-9), to].sort((a, b) => a - b);
|
||
const top = ts.map((t) => [Xs(t), Y(lv(t).top)] as [number, number]);
|
||
const bottom = ts.map((t) => [Xs(t), Y(lv(t).bottom)] as [number, number]).reverse();
|
||
const cutT = cutLeft ? from : to;
|
||
const cut = zig(Xs(cutT), Y(lv(cutT).top), Y(lv(cutT).bottom), cutLeft ? 1 : -1);
|
||
return cutLeft ? [...top, ...bottom, ...cut.reverse()] : [...top, ...cut, ...bottom];
|
||
};
|
||
const slabStyle = `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`;
|
||
const parts = [
|
||
poly([[X(wb), Y(low)], [X(0), Y(low)], [X(0), Y(0)], [X(pb), Y(0)], [X(pb), Y(-bH)], [X(wb), Y(-bH)]], `fill="${WALL}" stroke="${MUTED}"`),
|
||
poly(piece(t1, t1 + farLen, false), slabStyle),
|
||
poly(piece(tz, t0, true), slabStyle),
|
||
];
|
||
// 간격: 아스팔트 충진재 (위 30 %), 스티로폼 (아래).
|
||
const gTop = Y(st[0].top);
|
||
const gBot = Y(-bH);
|
||
const gMid = gTop + (gBot - gTop) * 0.3;
|
||
const gx = Xs(t0);
|
||
parts.push(`<rect x="${gx.toFixed(1)}" y="${gTop.toFixed(1)}" width="${(X(pb) - gx).toFixed(1)}" height="${(gMid - gTop).toFixed(1)}" fill="#3b3f45"/>`);
|
||
parts.push(`<rect x="${gx.toFixed(1)}" y="${gMid.toFixed(1)}" width="${(X(pb) - gx).toFixed(1)}" height="${(gBot - gMid).toFixed(1)}" fill="#ffffff" stroke="${MUTED}" stroke-width="0.5"/>`);
|
||
// 타르페이퍼 (브라켓 상면), 고무판 150 (가운데), 다웰 D25 in 강관 Φ60 L300.
|
||
const xc = (X(wb) + Xs(t0)) / 2;
|
||
parts.push(line(X(wb), Y(-bH), X(pb), Y(-bH), 'stroke="#8a5a2b" stroke-width="2.4"'));
|
||
parts.push(`<rect x="${(xc - 0.075 * k).toFixed(1)}" y="${(Y(-bH) - 3).toFixed(1)}" width="${(0.15 * k).toFixed(1)}" height="3" fill="#2b2b2b"/>`);
|
||
parts.push(`<rect x="${(xc - 0.03 * k).toFixed(1)}" y="${(Y(-bH) - 3 - 0.3 * k).toFixed(1)}" width="${(0.06 * k).toFixed(1)}" height="${(0.3 * k).toFixed(1)}" fill="none" stroke="${INK}" stroke-width="0.9"/>`);
|
||
parts.push(line(xc, Y(-bH) - 0.34 * k, xc, Y(-bH) + 0.3 * k, `stroke="${INK}" stroke-width="1.8"`));
|
||
// 치수.
|
||
const yRow = Y(-bH) + 18;
|
||
if (d.form === 'exposed') parts.push(dimLine([X(he), Y(lv(he).bottom)], [X(wb), Y(-bH)], yRow, 'h', `헌치 ${d.haunchLengthMm}`, true));
|
||
// 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (예: 300 − 5 = 295).
|
||
parts.push(dimLine([X(wb), Y(-bH)], [Xs(t0), Y(-bH)], yRow, 'h', `${Math.round((t0 - wb) * 1000)}`, true));
|
||
const yLen = Y(low) + 22;
|
||
parts.push(dimLine([X(t1), Y(lv(t1).bottom)], [Xs(t0), Y(st[0].bottom)], yLen, 'h', `${d.lengthMm} (중간 생략)`, true));
|
||
parts.push(dimLine([X(t1), Y(lv(t1).top)], [X(t1), Y(lv(t1).bottom)], X(t1) - 12, 'v', `${d.thicknessMm}`));
|
||
parts.push(dimLine([X(0), Y(0)], [X(0), Y(-bH)], X(0) + 16, 'v', `${Math.round(bH * 1000)}`, true));
|
||
// 글자: 경사, EL, 부속재 (오른쪽 · 왼쪽 아래로 끌어냄).
|
||
const xMidNear = (X(tz) + Xs(t0)) / 2;
|
||
parts.push(text(xMidNear, Y(lv(tz).top) - 12, `경사 ${d.slopeLabel}`, 'middle'));
|
||
const xr = X(0) + 58;
|
||
const lead = (x1: number, y1: number, x2: number, y2: number, s: string, anchor: 'start' | 'end') =>
|
||
line(x1, y1, x2, y2, `stroke="${MUTED}" stroke-width="0.7"`) + text(x2 + (anchor === 'start' ? 3 : -3), y2 + 4, s, anchor);
|
||
parts.push(text(xr, Y(0) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||
parts.push(lead(gx + 2, gTop + 4, xr - 4, Y(0) + 26, `간격 ${d.parapetGapMm} · 아스팔트 충진재`, 'start'));
|
||
parts.push(lead(gx + 2, gMid + 8, xr - 4, Y(0) + 48, '스티로폼', 'start'));
|
||
parts.push(text(xr, Y(-bH) + 4, `브라켓 상면 ${el(d.parapetTopEl - bH)}`, 'start'));
|
||
const xl = X(wb) - 24;
|
||
parts.push(lead(xc - 6, Y(-bH) - 1, xl, Y(-bH) + 46, '고무판 150 × 150 × 15', 'end'));
|
||
parts.push(lead(X(wb) + 4, Y(-bH) + 1, xl, Y(-bH) + 64, '타르페이퍼 T = 15', 'end'));
|
||
parts.push(lead(xc, Y(-bH) + 0.25 * k, xl, Y(-bH) + 82, '다웰 D25 @400 · 강관 Φ60 L300', 'end'));
|
||
parts.push(text(X(0) + 6, Y(low) - 4, '흉벽 · 벽체', 'start', MUTED));
|
||
const W = xr + 200;
|
||
const H = yLen + 24;
|
||
return [
|
||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="접속슬래브 단면 · 접합 상세 · ${d.form === 'exposed' ? '노출형' : '매립형'} · 경사 ${d.slopeLabel} · 길이 ${d.lengthMm} (중간 생략) · 두께 ${d.thicknessMm} · 흉벽 높이 ${Math.round(bH * 1000)}">`,
|
||
...parts,
|
||
'</svg>',
|
||
].join('');
|
||
}
|