feat(abut): 날개벽 측면 삽도에 흉벽 · 브라켓 단차 · 저판을 함께 그리고 교대 삽도 EL 표기를 EL.xx.xxx로 통일
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
886347471e
commit
41b3165543
7 files changed
+83
-36
No files matched your search
@@ -1094,6 +1094,14 @@ function fillAbutWingCard(
|
||||
bands: wing.bands,
|
||||
seatEl: rec.seatEl,
|
||||
footingTopEl: members.footingTopEl,
|
||||
parapetTopEl: wing.topStartEl,
|
||||
bracketHeightM: members.shape.bracketHeightM,
|
||||
wallBackM: members.shape.wallBackM,
|
||||
parapetBackM: members.shape.parapetBackM,
|
||||
wallFrontM: members.shape.wallFrontM,
|
||||
footingFrontM: members.shape.footingFrontM,
|
||||
footingBackM: members.shape.footingBackM,
|
||||
footingThicknessM: members.shape.footingThicknessM,
|
||||
}),
|
||||
`${sideKo(side)} 날개벽 측면 · ${NAME[wing.form]} (날개벽 축 방향으로 잘라 본 모습) · 축척 같음`,
|
||||
);
|
||||
|
||||
@@ -78,8 +78,8 @@ export function abutBearingSectionSvg(d: AbutBearingDetail): string {
|
||||
text(right, at(d.h1 + d.h2 + d.h3 / 2) + 4, `h3 ${d.h3}`, 'start'),
|
||||
text(right, at(E - d.h4 / 2) + 4, `h4 ${d.h4}`, 'start'),
|
||||
text(cx, at(E) - 10, `E = ${E}`),
|
||||
text(24, base + 16, `교좌면 ${d.seatEl.toFixed(3)}`, 'start', MUTED),
|
||||
text(24, at(d.h1 + d.h2) - 4, `z ${d.z.toFixed(3)}`, 'start', MUTED),
|
||||
text(24, base + 16, `교좌면 EL.${d.seatEl.toFixed(3)}`, 'start', MUTED),
|
||||
text(24, at(d.h1 + d.h2) - 4, `z EL.${d.z.toFixed(3)}`, 'start', MUTED),
|
||||
text(cx, 244, '교축 방향 · 높이 2배 과장', 'middle', MUTED),
|
||||
'</svg>',
|
||||
].join('');
|
||||
|
||||
+12
-10
@@ -35,6 +35,8 @@ const CONC = '#d9c9a8';
|
||||
const CONC_DARK = '#c7b48d';
|
||||
const FOOT = '#b9c3ce';
|
||||
const t3 = (v: number) => v.toFixed(3);
|
||||
/** EL 표기 (2026-11-04): "EL.41.995". */
|
||||
const el = (v: number) => `EL.${v.toFixed(3)}`;
|
||||
const label = (x: number, y: number, s: string, anchor = 'middle', 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}/>`;
|
||||
@@ -75,12 +77,12 @@ export function abutWallSectionSvg(d: AbutWallDrawing): string {
|
||||
`<line x1="${X(0).toFixed(1)}" y1="4" x2="${X(0).toFixed(1)}" y2="${(H - 30).toFixed(1)}" stroke="#c0392b" stroke-dasharray="6 3"/>`,
|
||||
label(X(0), 14, '교대축', 'middle', '#c0392b'),
|
||||
label((X(d.wallBackM) + X(d.wallFrontM)) / 2, Y(top) - 18, `${d.bracketWidthMm} + ${d.parapetThicknessMm} + ${d.seatWidthMm} = ${d.bracketWidthMm + d.parapetThicknessMm + d.seatWidthMm}`),
|
||||
label(X(0) + 6, Y(top) - 4, `흉벽 상단 ${t3(top)}`, 'start'),
|
||||
label(X(d.wallBackM) - 6, Y(bracket) + 4, `브라켓 상면 ${t3(bracket)}`, 'end'),
|
||||
label(X(d.wallFrontM) + 6, Y(seg.seatEl) + 4, `교좌면 ${t3(seg.seatEl)}`, 'start'),
|
||||
label(X(0) + 6, Y(top) - 4, `흉벽 상단 ${el(top)}`, 'start'),
|
||||
label(X(d.wallBackM) - 6, Y(bracket) + 4, `브라켓 상면 ${el(bracket)}`, 'end'),
|
||||
label(X(d.wallFrontM) + 6, Y(seg.seatEl) + 4, `교좌면 ${el(seg.seatEl)}`, 'start'),
|
||||
label(X(d.wallFrontM) + 6, Y((seg.seatEl + d.footingTopEl) / 2) + 4, `벽체 ${(seg.seatEl - d.footingTopEl).toFixed(3)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingTopEl) + 4, `상면 ${t3(d.footingTopEl)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingBottomEl) + 4, `기초 하단 ${t3(d.footingBottomEl)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingTopEl) + 4, `상면 ${el(d.footingTopEl)}`, 'start'),
|
||||
label(X(d.footingFrontM) + 6, Y(d.footingBottomEl) + 4, `기초 하단 ${el(d.footingBottomEl)}`, 'start'),
|
||||
label(X(d.footingBackM), H - 8, '← 배면', 'start', MUTED),
|
||||
label(X(d.footingFrontM), H - 8, '전면 →', 'end', MUTED),
|
||||
'</svg>',
|
||||
@@ -118,11 +120,11 @@ export function abutWallElevationSvg(d: AbutWallDrawing): string {
|
||||
}
|
||||
const first = d.segments.find((g) => d.lengthM >= g.sFrom && d.lengthM <= g.sTo) ?? d.segments[d.segments.length - 1];
|
||||
const last = d.segments.find((g) => g.sFrom <= 0 && g.sTo >= 0) ?? d.segments[0];
|
||||
parts.push(label(X(d.lengthM), Y(d.parapetTopEndEl) - 6, t3(d.parapetTopEndEl), 'start'));
|
||||
parts.push(label(X(0), Y(d.parapetTopStartEl) - 6, t3(d.parapetTopStartEl), 'end'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(first.seatEl) + 4, t3(first.seatEl), 'end'));
|
||||
parts.push(label(X(0) + 4, Y(last.seatEl) + 4, t3(last.seatEl), 'start'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(d.footingBottomEl) + 4, t3(d.footingBottomEl), 'end'));
|
||||
parts.push(label(X(d.lengthM), Y(d.parapetTopEndEl) - 6, el(d.parapetTopEndEl), 'start'));
|
||||
parts.push(label(X(0), Y(d.parapetTopStartEl) - 6, el(d.parapetTopStartEl), 'end'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(first.seatEl) + 4, el(first.seatEl), 'end'));
|
||||
parts.push(label(X(0) + 4, Y(last.seatEl) + 4, el(last.seatEl), 'start'));
|
||||
parts.push(label(X(d.lengthM) - 4, Y(d.footingBottomEl) + 4, el(d.footingBottomEl), 'end'));
|
||||
parts.push(label(X(d.lengthM), H - 8, 'IP3', 'middle', MUTED));
|
||||
parts.push(label(X(0), H - 8, 'IP1', 'middle', MUTED));
|
||||
parts.push(label((X(0) + X(d.lengthM)) / 2, H - 8, `${d.lengthM.toFixed(3)} m`));
|
||||
|
||||
+48
-17
@@ -68,46 +68,77 @@ export interface AbutWingSideDrawing {
|
||||
bands: { name: string; zTopEl: number; zBottomEl: number }[];
|
||||
seatEl: number;
|
||||
footingTopEl: number;
|
||||
/** 흉벽 상단 EL at this wing's end, and the 브라켓 높이 (m): 브라켓 상면 = 흉벽 상단 − 브라켓 높이. */
|
||||
parapetTopEl: number;
|
||||
bracketHeightM: number;
|
||||
/** 단면 위치 t (m, 전면 +): 벽체 배면 · 흉벽 배면 · 벽체 전면, 저판 앞굽 끝 · 뒷굽 끝, 저판 두께. */
|
||||
wallBackM: number;
|
||||
parapetBackM: number;
|
||||
wallFrontM: number;
|
||||
footingFrontM: number;
|
||||
footingBackM: number;
|
||||
footingThicknessM: number;
|
||||
}
|
||||
|
||||
/** 측면 (날개벽 축 방향으로 잘라 본 모습): 벽체 쪽이 오른쪽, 날개벽 끝이 왼쪽. 수평 단 경계와 뒷굽 끝을 표시한다. */
|
||||
/** EL 표기 (2026-11-04): "EL.41.995". */
|
||||
const el = (v: number) => `EL.${v.toFixed(3)}`;
|
||||
|
||||
/**
|
||||
* 측면 (날개벽 축 방향으로 잘라 본 모습): 벽체 쪽이 오른쪽, 날개벽 끝이 왼쪽.
|
||||
* 벽체 · 흉벽 · 브라켓 단차와 저판을 단면으로 함께 그린다. 가로 위치는 t를 날개벽 거리로 바꾼 값(dM = −t / k, k = 뒷굽 / 뒷굽 거리).
|
||||
*/
|
||||
export function abutWingSideSvg(d: AbutWingSideDrawing): string {
|
||||
const k = 40;
|
||||
const els = [...d.topLine.map((p) => p.el), ...d.bottomLine.map((p) => p.el), d.footingTopEl];
|
||||
const kt = Math.abs(d.footingBackM) / d.heelDistanceM; // |t| per m along the wing
|
||||
const dmOf = (tM: number) => -tM / kt;
|
||||
const bracketTop = d.parapetTopEl - d.bracketHeightM;
|
||||
const footingBottom = d.footingTopEl - d.footingThicknessM;
|
||||
const els = [...d.topLine.map((p) => p.el), ...d.bottomLine.map((p) => p.el), d.parapetTopEl, footingBottom];
|
||||
const elMax = Math.max(...els) + 0.8;
|
||||
const elMin = Math.min(...els) - 0.6;
|
||||
const X = (dM: number) => 40 + (d.lengthM - dM) * k; // wall (dM 0) on the right
|
||||
const Y = (el: number) => 14 + (elMax - el) * k;
|
||||
const W = X(-1.2) + 70;
|
||||
const H = Y(elMin) + 26;
|
||||
const elMin = Math.min(...els) - 0.5;
|
||||
const dmRight = dmOf(d.footingFrontM); // 저판 앞굽 끝 (rightmost)
|
||||
const X = (dM: number) => 40 + (d.lengthM - dM) * k; // wall (dM 0) toward the right
|
||||
const Y = (y: number) => 14 + (elMax - y) * k;
|
||||
const W = X(dmRight) + 110;
|
||||
const H = Y(elMin) + 30;
|
||||
const rect = (dm0: number, dm1: number, e0: number, e1: number, style: string) =>
|
||||
poly([[X(dm0), Y(e0)], [X(dm1), Y(e0)], [X(dm1), Y(e1)], [X(dm0), Y(e1)]], style);
|
||||
const outline: [number, number][] = [
|
||||
...d.topLine.map((p) => [X(p.dM), Y(p.el)] as [number, number]),
|
||||
...[...d.bottomLine].reverse().map((p) => [X(p.dM), Y(p.el)] as [number, number]),
|
||||
];
|
||||
const wb = dmOf(d.wallBackM);
|
||||
const pb = dmOf(d.parapetBackM);
|
||||
const wf = dmOf(d.wallFrontM);
|
||||
const parts = [
|
||||
// 벽체 (단면) on the right, 저판 under it.
|
||||
poly([[X(0), Y(d.footingTopEl)], [X(-1.0), Y(d.footingTopEl)], [X(-1.0), Y(d.seatEl)], [X(0), Y(d.seatEl)]], `fill="${WALL}" stroke="${MUTED}"`),
|
||||
poly([[X(Math.min(d.heelDistanceM, d.lengthM)), Y(d.footingTopEl)], [X(-1.2), Y(d.footingTopEl)], [X(-1.2), Y(d.footingTopEl - 0.5)], [X(Math.min(d.heelDistanceM, d.lengthM)), Y(d.footingTopEl - 0.5)]], `fill="#c9d1da" stroke="${MUTED}"`),
|
||||
poly(outline, `fill="${WING}" stroke="#9c8763"`),
|
||||
// 저판 (뒷굽 끝 ~ 앞굽 끝), 벽체 (기초 상면 ~ 교좌면), 브라켓 단차 (벽체 배면 ~ 흉벽 배면, 브라켓 상면까지), 흉벽 (흉벽 상단까지).
|
||||
rect(dmOf(d.footingBackM), dmRight, footingBottom, d.footingTopEl, `fill="#c9d1da" stroke="${MUTED}"`),
|
||||
rect(wb, wf, d.footingTopEl, d.seatEl, `fill="${WALL}" stroke="${MUTED}"`),
|
||||
rect(wb, pb, d.seatEl, bracketTop, `fill="#9fb1c0" stroke="${MUTED}"`),
|
||||
rect(pb, 0, d.seatEl, d.parapetTopEl, `fill="#9fb1c0" stroke="${MUTED}"`),
|
||||
];
|
||||
// 수평 단 경계 (등단면 / 변화단면) as dashed lines across the wing.
|
||||
for (const b of d.bands.slice(0, -1)) {
|
||||
parts.push(`<line x1="${X(d.lengthM).toFixed(1)}" y1="${Y(b.zBottomEl).toFixed(1)}" x2="${X(0).toFixed(1)}" y2="${Y(b.zBottomEl).toFixed(1)}" stroke="${MUTED}" stroke-dasharray="4 3"/>`);
|
||||
parts.push(`<line x1="${X(d.lengthM).toFixed(1)}" y1="${Y(b.zBottomEl).toFixed(1)}" x2="${X(wb).toFixed(1)}" y2="${Y(b.zBottomEl).toFixed(1)}" stroke="${MUTED}" stroke-dasharray="4 3"/>`);
|
||||
}
|
||||
for (const b of d.bands) {
|
||||
const mid = (b.zTopEl + b.zBottomEl) / 2;
|
||||
parts.push(text(X(d.lengthM * 0.45), Y(mid) + 4, b.name, 'middle', MUTED));
|
||||
parts.push(text(X(d.lengthM * 0.55), Y(mid) + 4, b.name, 'middle', MUTED));
|
||||
}
|
||||
parts.push(text((X(d.lengthM) + X(0)) / 2, 11, `${Math.round(d.lengthM * 1000)} (교좌면 끝에서, 날개벽 축 방향)`, 'middle'));
|
||||
if (d.form === 'general') parts.push(text(X(d.lengthM) - 4, (Y(d.topLine[d.topLine.length - 1].el) + Y(d.bottomLine[d.bottomLine.length - 1].el)) / 2 + 4, `${Math.round(d.endHeightM * 1000)}`, 'end'));
|
||||
parts.push(text(X(-1.0) + 4, Y(d.seatEl) + 4, `교좌면 ${d.seatEl.toFixed(3)}`, 'start'));
|
||||
parts.push(text(X(-1.2) + 4, Y(d.footingTopEl) + 14, d.footingTopEl.toFixed(3), 'start'));
|
||||
parts.push(text(X(0) + 4, Y(d.parapetTopEl) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start'));
|
||||
parts.push(text(X(pb) + 4, Y(bracketTop) - 4, `브라켓 상면 ${el(bracketTop)}`, 'start'));
|
||||
parts.push(text(X(wf) + 4, Y(d.seatEl) + 4, `교좌면 ${el(d.seatEl)}`, 'start'));
|
||||
parts.push(text(X(dmRight) + 4, Y(d.footingTopEl) + 4, el(d.footingTopEl), 'start'));
|
||||
parts.push(text(X(dmRight) + 4, Y(footingBottom) + 4, `기초 하단 ${el(footingBottom)}`, 'start'));
|
||||
if (d.heelDistanceM < d.lengthM) {
|
||||
parts.push(`<line x1="${X(d.heelDistanceM).toFixed(1)}" y1="${Y(d.footingTopEl).toFixed(1)}" x2="${X(d.heelDistanceM).toFixed(1)}" y2="${(Y(d.footingTopEl) + 18).toFixed(1)}" stroke="${INK}"/>`);
|
||||
parts.push(`<line x1="${X(d.heelDistanceM).toFixed(1)}" y1="${Y(footingBottom).toFixed(1)}" x2="${X(d.heelDistanceM).toFixed(1)}" y2="${(Y(footingBottom) + 10).toFixed(1)}" stroke="${INK}"/>`);
|
||||
}
|
||||
parts.push(text(X(Math.min(d.heelDistanceM, d.lengthM)), Y(d.footingTopEl) + 30, `뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m`, 'middle'));
|
||||
parts.push(text(X(Math.min(d.heelDistanceM, d.lengthM)), Y(footingBottom) + 22, `뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m`, 'middle'));
|
||||
return [
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="${d.name} 측면: 길이 ${Math.round(d.lengthM * 1000)}, 뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m">`,
|
||||
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="${d.name} 측면: 길이 ${Math.round(d.lengthM * 1000)}, 흉벽 상단 ${el(d.parapetTopEl)}, 브라켓 상면 ${el(bracketTop)}, 뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('교대 받침 삽도 (시안 ② 교량받침 카드)', () => {
|
||||
|
||||
it('단면: the four layers and E = h1 + h2 + h3 + h4 = 400', () => {
|
||||
const svg = abutBearingSectionSvg(d);
|
||||
for (const s of ['h1 75', 'h2 50', 'h3 250', 'h4 25', 'E = 400', '교좌면 44.367', 'z 44.492']) expect(svg).toContain(s);
|
||||
for (const s of ['h1 75', 'h2 50', 'h3 250', 'h4 25', 'E = 400', '교좌면 EL.44.367', 'z EL.44.492']) expect(svg).toContain(s);
|
||||
expect(svg.match(/<rect /g)).toHaveLength(4);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,16 +35,16 @@ const d: AbutWallDrawing = {
|
||||
describe('교대 벽체·흉벽 삽도 (시안 ② 벽체·흉벽 카드)', () => {
|
||||
it('단면 (① 구간): the width line and the levels of the generated model', () => {
|
||||
const svg = abutWallSectionSvg(d);
|
||||
for (const s of ['300 + 500 + 1600 = 2400', '교좌면 44.367', '벽체 2.372', '상면 41.995', '기초 하단 40.895', '교대축', '← 배면', '전면 →']) {
|
||||
for (const s of ['300 + 500 + 1600 = 2400', '교좌면 EL.44.367', '벽체 2.372', '상면 EL.41.995', '기초 하단 EL.40.895', '교대축', '← 배면', '전면 →']) {
|
||||
expect(svg).toContain(s);
|
||||
}
|
||||
expect(svg).toMatch(/흉벽 상단 4[67]\.\d{3}/);
|
||||
expect(svg).toMatch(/브라켓 상면 4[56]\.\d{3}/);
|
||||
expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/);
|
||||
expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/);
|
||||
});
|
||||
|
||||
it('정면·배면도: five steps, five bearings, the sloped 흉벽 top (IP3 46.835 → IP1 47.180) and the 수축줄눈', () => {
|
||||
const svg = abutWallElevationSvg(d);
|
||||
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', '46.835', '47.180', '①', '⑤']) expect(svg).toContain(s);
|
||||
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', 'EL.46.835', 'EL.47.180', '①', '⑤']) expect(svg).toContain(s);
|
||||
expect(svg).toContain('교좌면 계단 5구간');
|
||||
});
|
||||
|
||||
|
||||
@@ -51,14 +51,20 @@ describe('날개벽 삽도 (시안 ② 날개벽 카드)', () => {
|
||||
expect(svg.match(/<polygon /g)).toHaveLength(4); // slab, wall, two wings
|
||||
});
|
||||
|
||||
it('측면: the bands, the length, the end height and the 뒷굽 끝', () => {
|
||||
it('측면: the bands, the length, the end height, the 뒷굽 끝, and the 벽체 · 흉벽 · 브라켓 · 저판 with EL. labels', () => {
|
||||
const { members, wings } = build();
|
||||
const w = wings[0];
|
||||
const svg = abutWingSideSvg({
|
||||
name: '좌 날개벽', form: w.form, lengthM: w.lengthM, endHeightM: w.endHeightM, heelDistanceM: w.heelDistanceM,
|
||||
topLine: w.topLine, bottomLine: w.bottomLine, bands: w.bands, seatEl: rec.seatEl, footingTopEl: members.footingTopEl,
|
||||
parapetTopEl: w.topStartEl, bracketHeightM: members.shape.bracketHeightM, wallBackM: members.shape.wallBackM,
|
||||
parapetBackM: members.shape.parapetBackM, wallFrontM: members.shape.wallFrontM, footingFrontM: members.shape.footingFrontM,
|
||||
footingBackM: members.shape.footingBackM, footingThicknessM: members.shape.footingThicknessM,
|
||||
});
|
||||
for (const s of ['6500 (교좌면 끝에서, 날개벽 축 방향)', '1500', '등단면', '변화단면1', '변화단면2', '뒷굽 끝 1.807 m', '교좌면 44.367']) expect(svg).toContain(s);
|
||||
for (const s of ['6500 (교좌면 끝에서, 날개벽 축 방향)', '1500', '등단면', '변화단면1', '변화단면2', '뒷굽 끝 1.807 m', '교좌면 EL.44.367', 'EL.41.995', '기초 하단 EL.40.895']) expect(svg).toContain(s);
|
||||
expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/);
|
||||
expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/);
|
||||
expect(svg.match(/<polygon /g)).toHaveLength(5); // wing, 저판, 벽체, 브라켓 단차, 흉벽
|
||||
});
|
||||
|
||||
it('모서리: haunch, bracket, slab chamfer and gaps from the inputs', () => {
|
||||
|
||||
Reference in new issue
Block a user