feat(abut): 접속슬래브 경사는 형식이 정함, 단면 · 접합 상세를 파단선 단면 하나로 합침, 흉벽 높이 입력
- 경사 선택 삭제: 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (노트 표기). 예전 파일의 slope 값은 읽고 무시 - 단면 + 접합 상세: 흉벽 쪽과 먼 끝만 그리고 중간 생략, 부속재 · 치수(7000 · 450 · 850 · 295 · 헌치) · EL을 한 그림에 - 흉벽 높이 (브라켓 상면 ~ 흉벽 상단) 입력 = 벽체·흉벽 카드 브라켓 높이와 같은 값 - 자동 값: 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (295) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b8b80ae97
commit
9835fb42b3
7 files changed
+157
-199
No files matched your search
+79
-95
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이), 단면(교대축 직각, 매립형 / 노출형), 흉벽–슬래브 접합 상세.
|
||||
* 순수 문자열 함수. 평면·단면은 생성본 값으로 그리고 가로·세로 축척이 같다. 접합 상세는 부속재를 보여 주는 모식도다(표시용).
|
||||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이)과 단면 + 접합 상세(매립형 / 노출형, 중간 생략).
|
||||
* 순수 문자열 함수. 생성본 값으로 그리고 가로·세로 축척이 같다(단면은 파단선으로 중간을 생략). 접합 부속재는 표시용이다.
|
||||
*/
|
||||
import type { AbutPlanOutline } from './abut-scene';
|
||||
import type { SlabForm, SlabStation } from '../core/abut/slab';
|
||||
import { slabLevelAt, type SlabForm, type SlabStation } from '../core/abut/slab';
|
||||
import { dimLine } from './abut-wing-svg';
|
||||
|
||||
const INK = '#18212b';
|
||||
@@ -92,108 +92,92 @@ export interface AbutSlabSectionDrawing {
|
||||
haunchLengthMm: number;
|
||||
}
|
||||
|
||||
/** 단면: 날개벽 축 방향(교대축 직각), 왼쪽이 배면. 흉벽 · 브라켓 단차, 슬래브(매립형 일정 두께 / 노출형 헌치), 치수와 EL. */
|
||||
/**
|
||||
* 단면 + 접합 상세 (2026-11-04, 그림 합침): 교대축 직각, 왼쪽이 배면. 흉벽 쪽(헌치 끝 너머까지)과 먼 끝 0.8 m만 같은 축척으로
|
||||
* 그리고 그 사이는 파단선으로 생략한다. 간격(5)과 고무판 두께는 보이도록 키워 그리고, 부속재(충진재 · 스티로폼 · 고무판 ·
|
||||
* 타르페이퍼 · 다웰 · 강관)는 표시용이다. 치수: 7000 (중간 생략), 450, 흉벽 높이(브라켓 상면 ~ 흉벽 상단), 헌치, 브라켓 얹힘 길이.
|
||||
*/
|
||||
export function abutSlabSectionSvg(d: AbutSlabSectionDrawing): string {
|
||||
const k = 56;
|
||||
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 low = -d.bracketHeightM - 0.9;
|
||||
const hi = Math.max(0, ...st.map((p) => p.top)) + 0.5;
|
||||
const minBottom = Math.min(...st.map((p) => p.bottom));
|
||||
const left = t1 - 1.0;
|
||||
const X = (t: number) => 20 + (t - left) * k;
|
||||
const Y = (y: number) => 12 + (hi - y) * k;
|
||||
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 = [
|
||||
// 흉벽 (t 흉벽 배면 … 0) and the 브라켓 단차 (벽체 배면 … 흉벽 배면), cut below.
|
||||
poly(
|
||||
[[X(d.wallBackM), Y(low)], [X(0), Y(low)], [X(0), Y(0)], [X(d.parapetBackM), Y(0)], [X(d.parapetBackM), Y(-bH)], [X(d.wallBackM), Y(-bH)]],
|
||||
`fill="${WALL}" stroke="${MUTED}"`,
|
||||
),
|
||||
poly([...st.map((p) => [X(p.t), Y(p.top)] as [number, number]), ...[...st].reverse().map((p) => [X(p.t), Y(p.bottom)] as [number, number])], `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`),
|
||||
];
|
||||
// 다웰 D25 at the middle of the 브라켓 width.
|
||||
const dowelT = (d.wallBackM + d.parapetBackM) / 2;
|
||||
parts.push(line(X(dowelT), Y(-bH - 0.35), X(dowelT), Y(-bH + 0.2), `stroke="${INK}" stroke-width="1.6"`));
|
||||
parts.push(text(X(d.wallBackM) - 6, Y(-bH - 0.45), '다웰 D25 @400', 'end', MUTED));
|
||||
// 치수: 길이 (흉벽 · 벽체 아래), 끝 두께 (왼쪽 끝), 노출형은 흉벽 쪽 끝 두께와 헌치 길이.
|
||||
const yDim = Math.max(Y(minBottom), Y(low)) + 18;
|
||||
parts.push(dimLine([X(t1), Y(st[st.length - 1].bottom)], [X(t0), Y(st[0].bottom)], yDim, 'h', `${d.lengthMm}`, true));
|
||||
const far = st[st.length - 1];
|
||||
parts.push(dimLine([X(t1), Y(far.top)], [X(t1), Y(far.bottom)], X(t1) - 10, 'v', `${d.thicknessMm}`));
|
||||
if (d.form === 'exposed') {
|
||||
const he = d.wallBackM - d.haunchLengthMm / 1000;
|
||||
const heP = st.find((p) => Math.abs(p.t - he) < 1e-6);
|
||||
if (heP) parts.push(dimLine([X(he), Y(heP.bottom)], [X(d.wallBackM), Y(-bH)], yDim + 22, 'h', `헌치 ${d.haunchLengthMm}`, true));
|
||||
parts.push(text(X(dowelT) - 6, Y((st[0].top + st[0].bottom) / 2) + 4, `끝 ${Math.round(bH * 1000)}`, 'end'));
|
||||
}
|
||||
// 경사 (상면 가운데 위), 간격, EL.
|
||||
const mid = st[Math.floor((st.length - 1) / 2)];
|
||||
parts.push(text((X(t1) + X(mid.t)) / 2, Y(Math.max(far.top, mid.top)) - 8, `경사 ${d.slopeLabel}`, 'middle'));
|
||||
parts.push(text(X(t0), Y(st[0].top) - 8, `간격 ${d.parapetGapMm}`, 'middle', MUTED));
|
||||
parts.push(text(X(0) + 6, Y(0) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(text(X(0) + 6, Y(-bH) + 4, `브라켓 상면 ${el(d.parapetTopEl - bH)}`, 'start'));
|
||||
parts.push(text(X(0) + 6, Y(low) - 4, '흉벽 · 벽체', 'start', MUTED));
|
||||
const W = X(0) + 190;
|
||||
const H = yDim + (d.form === 'exposed' ? 44 : 22);
|
||||
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}">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
export interface AbutSlabJointDrawing {
|
||||
form: SlabForm;
|
||||
/** 경사 (%, + = 배면 쪽으로 올라감). "S = 4.00 % (하향)"처럼 표시한다. */
|
||||
gradePct: number;
|
||||
parapetTopEl: number;
|
||||
bracketHeightM: number;
|
||||
parapetGapMm: number;
|
||||
thicknessMm: number;
|
||||
}
|
||||
|
||||
/** 흉벽–접속슬래브 접합 상세 (삽도 기준, 모식도): 간격 · 아스팔트 충진재 / 스티로폼, 고무판, 타르페이퍼, 다웰 · 강관. */
|
||||
export function abutSlabJointSvg(d: AbutSlabJointDrawing): string {
|
||||
// Fixed schematic (px): 흉벽 x 300–360 from y 20, 브라켓 상면 y 150 from x 260.
|
||||
const bracketY = 150;
|
||||
const padH = 4;
|
||||
const slabEnd = 296;
|
||||
const bottomY = bracketY - padH;
|
||||
const exposed = d.form === 'exposed';
|
||||
const topY = exposed ? 20 : 86;
|
||||
const drop = d.gradePct < 0 ? 10 : -6; // left end lower for a fall, higher for a rise
|
||||
const slab: [number, number][] = exposed
|
||||
? [[20, topY + drop + 60], [20, topY + drop], [slabEnd, topY], [slabEnd, bottomY], [150, topY + drop * 0.5 + 60]]
|
||||
: [[20, bottomY + drop], [20, topY + drop], [slabEnd, topY], [slabEnd, bottomY]];
|
||||
const gapMid = topY + (bottomY - topY) * 0.3;
|
||||
const parts = [
|
||||
poly([[260, 262], [360, 262], [360, 20], [300, 20], [300, bracketY], [260, bracketY]], `fill="${WALL}" stroke="${MUTED}"`),
|
||||
poly(slab, `fill="${SLAB}" stroke="${SLAB_EDGE}" stroke-width="1.4"`),
|
||||
// 간격: 아스팔트 충진재 (위), 스티로폼 (아래).
|
||||
`<rect x="${slabEnd}" y="${topY}" width="4" height="${(gapMid - topY).toFixed(1)}" fill="#3b3f45"/>`,
|
||||
`<rect x="${slabEnd}" y="${gapMid.toFixed(1)}" width="4" height="${(bottomY - gapMid).toFixed(1)}" fill="#ffffff" stroke="${MUTED}" stroke-width="0.6"/>`,
|
||||
// 고무판 150 × 150 × 15, 타르페이퍼 T15, 다웰 D25 in 강관 Φ60 L300.
|
||||
`<rect x="268" y="${bottomY}" width="24" height="${padH}" fill="#2b2b2b"/>`,
|
||||
line(260, bracketY, 300, bracketY, 'stroke="#8a5a2b" stroke-width="2.4"'),
|
||||
`<rect x="276" y="${bottomY - 40}" width="8" height="40" fill="none" stroke="${INK}" stroke-width="0.9"/>`,
|
||||
line(280, bottomY - 46, 280, bracketY + 48, `stroke="${INK}" stroke-width="1.8"`),
|
||||
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(lead(298, topY + 6, 372, 58, `간격 ${d.parapetGapMm} · 아스팔트 충진재`, 'start'));
|
||||
parts.push(lead(298, gapMid + 12, 372, 100, '스티로폼', 'start'));
|
||||
parts.push(lead(272, bottomY + 2, 190, 196, '고무판 150 × 150 × 15', 'end'));
|
||||
parts.push(lead(264, bracketY + 1, 190, 214, '타르페이퍼 T = 15', 'end'));
|
||||
parts.push(lead(280, bracketY + 40, 190, 232, '다웰 D25 · 강관 Φ60 L300', 'end'));
|
||||
parts.push(text(130, exposed ? 120 : (topY + bottomY) / 2 + 4, exposed ? `노출형 · 끝 ${Math.round(d.bracketHeightM * 1000)}` : `접속슬래브 ${d.thicknessMm}`, 'middle'));
|
||||
parts.push(text(40, topY + drop - 6, `S = ${Math.abs(d.gradePct).toFixed(2)} % (${d.gradePct < 0 ? '하향' : '상향'})`, 'start'));
|
||||
parts.push(text(368, 24, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(text(368, bracketY + 4, `브라켓 상면 ${el(d.parapetTopEl - d.bracketHeightM)}`, 'start'));
|
||||
parts.push(text(330, 252, '흉벽 · 벽체', 'middle', MUTED));
|
||||
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 -22 560 292" width="100%" role="img" aria-label="흉벽–접속슬래브 접합 상세 (모식도): 간격 ${d.parapetGapMm}, 고무판, 타르페이퍼, 다웰 D25">`,
|
||||
`<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('');
|
||||
|
||||
Reference in new issue
Block a user