feat(abut): 접속슬래브 카드 — 매립형 / 노출형 형식, 경사, 치수 입력, 삽도 3개, 모따기 3D 반영
- 노출형 (삽도 기준, QA-8 가정): 상면 = 흉벽 상단, 끝은 브라켓 위(끝 두께 = 브라켓 높이), 헌치 길이 기본 2000 - 형식을 고르면 경사가 기본값으로 바뀜 (매립형 하향 4 %, 노출형 종단경사), 경사 타일로 변경 가능 - 길이 · 두께 · 흉벽과 간격 · 날개벽과 간격 입력이 3D, 폭, 날개벽 평면에 반영 - 슬래브 모따기 = 날개벽 헌치 (3D와 평면), 날개벽 없는 측은 skew 교좌면 끝선 - 3D 슬래브를 단면 기준 입체로 다시 만듦, 프로젝트 파일 저장 · 검증 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fc0370883f
commit
4b8b80ae97
14 files changed
+854
-60
No files matched your search
+88
-37
@@ -9,35 +9,65 @@ import * as THREE from 'three';
|
||||
import type { AbutModel, AbutPoint } from '../core/abut/geometry';
|
||||
import { buildMembers, type AbutMembers } from '../core/abut/members';
|
||||
import { buildWings, type WingSolid } from '../core/abut/wings';
|
||||
import { resolveSlab, slabGradePct, slabLevelAt, slabSection, type AbutSlab } from '../core/abut/slab';
|
||||
import type { TerrainGrid } from '../core/ground-model';
|
||||
import { terrainGeometry } from './terrain';
|
||||
|
||||
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
|
||||
/** 접속슬래브 두께 (m), spec_abut §4.8. */
|
||||
const SLAB_THICKNESS_M = 0.45;
|
||||
/** 접속슬래브 길이 (배면 쪽, m), spec_abut §4.8 "7000 (날개벽 축 방향)". */
|
||||
const SLAB_LENGTH_M = 7;
|
||||
/** 접속슬래브 하향 경사 (배면 쪽으로), spec_abut §4.8 "하향 4 %". */
|
||||
const SLAB_FALL = 0.04;
|
||||
/** 접속슬래브와 날개벽 안쪽면 사이 간격 (m), spec_abut §4.8 "날개벽 20". */
|
||||
const SLAB_CLEARANCE_M = 0.02;
|
||||
/** 말뚝 두부 매입 (m): the drawn pile head goes this far into the 저판 (drawing only). */
|
||||
const PILE_HEAD_EMBED_M = 0.1;
|
||||
|
||||
/** Eight-corner slab with its top sloping away from the wall. Indexed by (s end, t end, bottom/top). Double-sided: the winding is not critical. */
|
||||
function slabGeometry(plan: [number, number][], bottomEl: (s: number, t: number) => number): THREE.BufferGeometry {
|
||||
// plan: four (s, t) corners in order; vertices 0–3 are the bottom, 4–7 the top.
|
||||
const corners = [...plan.map(([s, t]) => [s, bottomEl(s, t), t]), ...plan.map(([s, t]) => [s, bottomEl(s, t) + SLAB_THICKNESS_M, t])];
|
||||
const quads = [
|
||||
[0, 1, 2, 3], [4, 5, 6, 7], // bottom and top
|
||||
[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7], // sides
|
||||
];
|
||||
/** Keeps the part of a convex plan polygon with tLo ≤ t ≤ tHi (two half-plane cuts). */
|
||||
function clipT(poly: [number, number][], tHi: number, tLo: number): [number, number][] {
|
||||
const cut = (pts: [number, number][], keep: (t: number) => boolean, edge: number) => {
|
||||
const out: [number, number][] = [];
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const a = pts[i];
|
||||
const b = pts[(i + 1) % pts.length];
|
||||
if (keep(a[1])) out.push(a);
|
||||
if (keep(a[1]) !== keep(b[1])) {
|
||||
const k = (edge - a[1]) / (b[1] - a[1]);
|
||||
out.push([a[0] + k * (b[0] - a[0]), edge]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
return cut(cut(poly, (t) => t <= tHi + 1e-9, tHi), (t) => t >= tLo - 1e-9, tLo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 접속슬래브 solid (2026-11-04): the plan polygon (s, t), cut into strips at the section stations (t, descending). Inside a
|
||||
* strip the top and the bottom are flat, so each strip is fanned; the sides follow the plan edges, split at the stations.
|
||||
* Double-sided material: the winding is not critical.
|
||||
*/
|
||||
function slabGeometry(plan: [number, number][], stationsT: number[], level: (s: number, t: number) => { top: number; bottom: number }): THREE.BufferGeometry {
|
||||
const positions: number[] = [];
|
||||
const push = (s: number, y: number, t: number) => {
|
||||
positions.push(s, y, t);
|
||||
return positions.length / 3 - 1;
|
||||
};
|
||||
const indices: number[] = [];
|
||||
for (const q of quads) {
|
||||
const base = positions.length / 3;
|
||||
for (const i of q) positions.push(...corners[i]);
|
||||
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
||||
for (let i = 0; i + 1 < stationsT.length; i++) {
|
||||
const strip = clipT(plan, stationsT[i], stationsT[i + 1]);
|
||||
if (strip.length < 3) continue;
|
||||
for (const face of ['top', 'bottom'] as const) {
|
||||
const ids = strip.map(([s, t]) => push(s, level(s, t)[face], t));
|
||||
for (let j = 1; j + 1 < ids.length; j++) indices.push(ids[0], ids[j], ids[j + 1]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < plan.length; i++) {
|
||||
const a = plan[i];
|
||||
const b = plan[(i + 1) % plan.length];
|
||||
const cuts = stationsT.filter((t) => (t - a[1]) * (t - b[1]) < 0).map((t) => (t - a[1]) / (b[1] - a[1]));
|
||||
const ks = [0, ...cuts.sort((x, y) => x - y), 1];
|
||||
for (let j = 0; j + 1 < ks.length; j++) {
|
||||
const p = [a[0] + ks[j] * (b[0] - a[0]), a[1] + ks[j] * (b[1] - a[1])];
|
||||
const q = [a[0] + ks[j + 1] * (b[0] - a[0]), a[1] + ks[j + 1] * (b[1] - a[1])];
|
||||
const lp = level(p[0], p[1]);
|
||||
const lq = level(q[0], q[1]);
|
||||
const v = [push(p[0], lp.bottom, p[1]), push(q[0], lq.bottom, q[1]), push(q[0], lq.top, q[1]), push(p[0], lp.top, p[1])];
|
||||
indices.push(v[0], v[1], v[2], v[0], v[2], v[3]);
|
||||
}
|
||||
}
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
@@ -120,27 +150,44 @@ function wingDirectionAxis(model: AbutModel): { ws: number; wt: number } {
|
||||
return { ws: w.e * model.u.e + w.n * model.u.n, wt: w.e * front.e + w.n * front.n };
|
||||
}
|
||||
|
||||
/** Plan (s, t) outlines of the abutment: the 벽체 (skew), each wing, and the 접속슬래브 between the wings (spec §4.8). */
|
||||
/**
|
||||
* Plan (s, t) outlines of the abutment: the 벽체 (skew), each wing, and the 접속슬래브 between the wings (spec §4.8).
|
||||
* The slab starts 흉벽과 간격 behind the 흉벽 back face, runs 길이 back, keeps 날개벽과 간격 from each wing's inner face, and
|
||||
* is chamfered at the 흉벽 corners by that wing's 헌치 (2026-11-04: 모따기 = 날개벽 헌치). No wing: the 교좌면 끝선, no chamfer.
|
||||
*/
|
||||
export interface AbutPlanOutline {
|
||||
wall: [number, number][];
|
||||
wings: { side: 'left' | 'right'; sEnd: number; poly: [number, number][] }[];
|
||||
slab: [number, number][];
|
||||
}
|
||||
|
||||
export function abutPlanOutline(model: AbutModel, members: AbutMembers, wings: WingSolid[]): AbutPlanOutline {
|
||||
export function abutPlanOutline(model: AbutModel, members: AbutMembers, wings: WingSolid[], slab: AbutSlab = resolveSlab()): AbutPlanOutline {
|
||||
const L = model.length;
|
||||
const k = abutSkewK(model);
|
||||
const tb = members.shape.wallBackM;
|
||||
const tf = members.shape.wallFrontM;
|
||||
const near = members.shape.parapetBackM - 0.005;
|
||||
const far = near - SLAB_LENGTH_M;
|
||||
const innerS = (sEnd: number, t: number): number => {
|
||||
const w = wings.find((x) => x.sEnd === sEnd);
|
||||
if (!w) return sEnd; // no wing on this side: the slab ends at the 교좌면 끝선
|
||||
const a = w.sEnd === 0 ? 1 : -1;
|
||||
const near = members.shape.parapetBackM - slab.parapetGapMm / 1000;
|
||||
const far = near - slab.lengthMm / 1000;
|
||||
const sideK = (w: WingSolid) => {
|
||||
const p0 = toAxis(model, w.corners[0]);
|
||||
const p1 = toAxis(model, w.corners[1]);
|
||||
return sEnd + a * (w.thicknessM + SLAB_CLEARANCE_M) + ((p1.s - p0.s) / (p1.t - p0.t)) * t;
|
||||
return (p1.s - p0.s) / (p1.t - p0.t);
|
||||
};
|
||||
const innerS = (sEnd: number, t: number): number => {
|
||||
const w = wings.find((x) => x.sEnd === sEnd);
|
||||
if (!w) return sEnd + k * t; // no wing on this side: the slab ends at the 교좌면 끝선 (skew)
|
||||
const a = w.sEnd === 0 ? 1 : -1;
|
||||
return sEnd + a * (w.thicknessM + slab.wingGapMm / 1000) + sideK(w) * t;
|
||||
};
|
||||
// Near corner of one side: [point on the side line, point on the near edge] (chamfer), or the corner alone.
|
||||
const nearCorner = (sEnd: number): [number, number][] => {
|
||||
const w = wings.find((x) => x.sEnd === sEnd);
|
||||
const corner: [number, number] = [innerS(sEnd, near), near];
|
||||
if (!w || !(w.haunchM > 0)) return [corner];
|
||||
const c = w.haunchM;
|
||||
const a = sEnd === 0 ? 1 : -1;
|
||||
const tSide = near - c / Math.hypot(1, sideK(w));
|
||||
return [[innerS(sEnd, tSide), tSide], [corner[0] + a * c, near]];
|
||||
};
|
||||
const axis = (c: AbutPoint): [number, number] => {
|
||||
const p = toAxis(model, c);
|
||||
@@ -149,7 +196,7 @@ export function abutPlanOutline(model: AbutModel, members: AbutMembers, wings: W
|
||||
return {
|
||||
wall: [[k * tb, tb], [L + k * tb, tb], [L + k * tf, tf], [k * tf, tf]],
|
||||
wings: wings.map((w) => ({ side: w.side, sEnd: w.sEnd, poly: w.corners.map(axis) })),
|
||||
slab: [[innerS(0, near), near], [innerS(L, near), near], [innerS(L, far), far], [innerS(0, far), far]],
|
||||
slab: [...nearCorner(0), ...nearCorner(L).reverse(), [innerS(L, far), far], [innerS(0, far), far]],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,6 +227,7 @@ export function buildAbutGroup(
|
||||
pivot?: { e: number; n: number },
|
||||
groundEl?: number,
|
||||
terrain?: TerrainGrid,
|
||||
slabInput?: Partial<AbutSlab>,
|
||||
): THREE.Group {
|
||||
const root = new THREE.Group();
|
||||
root.name = model.name;
|
||||
@@ -289,16 +337,19 @@ export function buildAbutGroup(
|
||||
add('blocks', placed(prism([[-0.5, blockTop], [0.5, blockTop], [0.5, b.z], [-0.5, b.z]], 1.0, new THREE.Vector3(0, 0, -0.5), ex, ey, ez)), 0x6e7a86);
|
||||
}
|
||||
|
||||
// 접속슬래브 (spec §4.8): bottom on the 브라켓 top (흉벽 상단 − 0.85), 5 mm behind the 흉벽 back face, 7 m back, falling 4 % away from the wall.
|
||||
// Plan: between the wing inner faces, each 20 mm inside (spec §4.8), from abutPlanOutline (shared with the ② 날개벽 plan).
|
||||
const slabNear = members.shape.parapetBackM - 0.005;
|
||||
const slabPlan = abutPlanOutline(model, members, wings).slab;
|
||||
const bracketTopEl = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / L - members.shape.bracketHeightM;
|
||||
// 접속슬래브 (spec §4.8, 2026-11-04 매립형 / 노출형): the section (core/abut/slab.ts) is measured from the 흉벽 상단 at each s.
|
||||
// Plan: between the wing inner faces with the gaps and chamfers, from abutPlanOutline (shared with the ② 날개벽 plan).
|
||||
const slab = resolveSlab(slabInput);
|
||||
const slabPlan = abutPlanOutline(model, members, wings, slab).slab;
|
||||
const section = slabSection(slab, slabGradePct(slab, model.wing?.slopePct ?? 0), members.shape);
|
||||
const slabMesh = new THREE.Mesh(
|
||||
slabGeometry(
|
||||
slabPlan,
|
||||
// Flat on the bracket where it lies over the back strip (t ≥ 벽체 배면), falling 4 % only behind the wall.
|
||||
(s, t) => bracketTopEl(s) - SLAB_FALL * Math.max(0, members.shape.wallBackM - t),
|
||||
section.map((p) => p.t),
|
||||
(s, t) => {
|
||||
const lv = slabLevelAt(section, t);
|
||||
return { top: parapetTopAt(s) + lv.top, bottom: parapetTopAt(s) + lv.bottom };
|
||||
},
|
||||
),
|
||||
new THREE.MeshStandardMaterial({ color: 0xd8c8a0, roughness: 0.9, side: THREE.DoubleSide }),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 교대 접속슬래브 삽도 (2026-11-04, 시안 ② 접속슬래브 카드): 평면(날개벽 사이), 단면(교대축 직각, 매립형 / 노출형), 흉벽–슬래브 접합 상세.
|
||||
* 순수 문자열 함수. 평면·단면은 생성본 값으로 그리고 가로·세로 축척이 같다. 접합 상세는 부속재를 보여 주는 모식도다(표시용).
|
||||
*/
|
||||
import type { AbutPlanOutline } from './abut-scene';
|
||||
import type { SlabForm, 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;
|
||||
}
|
||||
|
||||
/** 단면: 날개벽 축 방향(교대축 직각), 왼쪽이 배면. 흉벽 · 브라켓 단차, 슬래브(매립형 일정 두께 / 노출형 헌치), 치수와 EL. */
|
||||
export function abutSlabSectionSvg(d: AbutSlabSectionDrawing): string {
|
||||
const k = 56;
|
||||
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 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"`),
|
||||
];
|
||||
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));
|
||||
return [
|
||||
`<svg viewBox="0 -22 560 292" width="100%" role="img" aria-label="흉벽–접속슬래브 접합 상세 (모식도): 간격 ${d.parapetGapMm}, 고무판, 타르페이퍼, 다웰 D25">`,
|
||||
...parts,
|
||||
'</svg>',
|
||||
].join('');
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function abutWingPlanSvg(d: AbutWingPlanDrawing): string {
|
||||
poly(map(d.outline.wall), `fill="${WALL}" stroke="${MUTED}"`),
|
||||
...d.outline.wings.map((w) => poly(map(w.poly), `fill="${WING}" stroke="#9c8763"`)),
|
||||
];
|
||||
const slabC = d.outline.slab.reduce((a, [s, t]) => [a[0] + s / 4, a[1] + t / 4], [0, 0]);
|
||||
const slabC = d.outline.slab.reduce((a, [s, t]) => [a[0] + s / d.outline.slab.length, a[1] + t / d.outline.slab.length], [0, 0]);
|
||||
parts.push(text(X(slabC[0]), Y(slabC[1]), `접속슬래브 폭 ${d.slabWidthM.toFixed(3)} m`, 'middle'));
|
||||
for (const w of d.outline.wings) {
|
||||
const far = w.poly[1];
|
||||
@@ -87,17 +87,19 @@ const el = (v: number) => `EL.${v.toFixed(3)}`;
|
||||
* 측면 (날개벽 축 방향으로 잘라 본 모습): 벽체 쪽이 오른쪽, 날개벽 끝이 왼쪽.
|
||||
* 벽체 · 흉벽 · 브라켓 단차와 저판을 단면으로 함께 그린다. 가로 위치는 t를 날개벽 거리로 바꾼 값(dM = −t / k, k = 뒷굽 / 뒷굽 거리).
|
||||
*/
|
||||
/** 치수선 (2026-11-04): 점 a, b에서 치수선 위치 at(가로면 y, 세로면 x)까지 보조선, 치수선, 양 끝 사선 틱, 치수. */
|
||||
function dimLine(a: [number, number], b: [number, number], at: number, dir: 'h' | 'v', label: string): string {
|
||||
/** 치수선 (2026-11-04): 점 a, b에서 치수선 위치 at(가로면 y, 세로면 x)까지 보조선, 치수선, 양 끝 사선 틱, 치수. after = 치수를 선의 아래 · 오른쪽에. */
|
||||
export function dimLine(a: [number, number], b: [number, number], at: number, dir: 'h' | 'v', label: string, after = false): string {
|
||||
const line = (x1: number, y1: number, x2: number, y2: number) =>
|
||||
`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${INK}" stroke-width="0.8"/>`;
|
||||
const tick = (x: number, y: number) => line(x - 3, y + 3, x + 3, y - 3);
|
||||
if (dir === 'h') {
|
||||
const ext = (x: number, y: number) => line(x, y + Math.sign(at - y) * 3, x, at + Math.sign(at - y) * 3);
|
||||
return [ext(a[0], a[1]), ext(b[0], b[1]), line(a[0], at, b[0], at), tick(a[0], at), tick(b[0], at), text((a[0] + b[0]) / 2, at - 4, label, 'middle')].join('');
|
||||
const lab = text((a[0] + b[0]) / 2, after ? at + 13 : at - 4, label, 'middle');
|
||||
return [ext(a[0], a[1]), ext(b[0], b[1]), line(a[0], at, b[0], at), tick(a[0], at), tick(b[0], at), lab].join('');
|
||||
}
|
||||
const ext = (x: number, y: number) => line(x + Math.sign(at - x) * 3, y, at + Math.sign(at - x) * 3, y);
|
||||
return [ext(a[0], a[1]), ext(b[0], b[1]), line(at, a[1], at, b[1]), tick(at, a[1]), tick(at, b[1]), text(at - 4, (a[1] + b[1]) / 2 + 4, label, 'end')].join('');
|
||||
const lab = after ? text(at + 4, (a[1] + b[1]) / 2 + 4, label, 'start') : text(at - 4, (a[1] + b[1]) / 2 + 4, label, 'end');
|
||||
return [ext(a[0], a[1]), ext(b[0], b[1]), line(at, a[1], at, b[1]), tick(at, a[1]), tick(at, b[1]), lab].join('');
|
||||
}
|
||||
|
||||
export function abutWingSideSvg(d: AbutWingSideDrawing): string {
|
||||
|
||||
Reference in new issue
Block a user