The plan has overall dimension lines (length along the top, width at the right) and two chains: along the length, the distances from the footing edge to the first pile and between piles; across the width, the same for the pile rows. The chains are read from the pile positions, so they follow the layout. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
138 lines
7.6 KiB
TypeScript
138 lines
7.6 KiB
TypeScript
/**
|
|
* 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views as SVG strings, drawn from the model (footings, piles,
|
|
* columns) and the input values. Pure: no DOM, so that it can be tested. Units: m in the model; the labels show mm.
|
|
* Both views use one scale (FOOTING_PX_PER_M), so they can sit side by side. The side view breaks the column and the piles
|
|
* (끊음 표시) so that its height stays near the plan's.
|
|
*/
|
|
import type { PierModel } from '../core/geometry';
|
|
|
|
export const FOOTING_PX_PER_M = 22;
|
|
/** Side view: the column is shown this far above the footing top, the piles this far below the footing bottom (m). */
|
|
const COLUMN_STUB = 3;
|
|
const PILE_STUB = 4;
|
|
|
|
const mm = (v: number) => Math.round(v * 1000);
|
|
|
|
/** Horizontal dimension line from x1 to x2 at height y, with 45° ticks and a centred label above it (승인 2026-10-23). */
|
|
function dimH(x1: number, x2: number, y: number, label: string): string {
|
|
const t = 0.12;
|
|
return `<g stroke="#2d3742" stroke-width="0.02" fill="#2d3742"><line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}"/><line x1="${x1 - t}" y1="${y + t}" x2="${x1 + t}" y2="${y - t}"/><line x1="${x2 - t}" y1="${y + t}" x2="${x2 + t}" y2="${y - t}"/><text x="${(x1 + x2) / 2}" y="${y - 0.15}" font-size="0.5" text-anchor="middle" stroke="none">${label}</text></g>`;
|
|
}
|
|
|
|
/** Vertical dimension line from y1 to y2 at x, with 45° ticks and the label to its right. */
|
|
function dimV(x: number, y1: number, y2: number, label: string): string {
|
|
const t = 0.12;
|
|
return `<g stroke="#2d3742" stroke-width="0.02" fill="#2d3742"><line x1="${x}" y1="${y1}" x2="${x}" y2="${y2}"/><line x1="${x - t}" y1="${y1 + t}" x2="${x + t}" y2="${y1 - t}"/><line x1="${x - t}" y1="${y2 + t}" x2="${x + t}" y2="${y2 - t}"/><text x="${x + 0.15}" y="${(y1 + y2) / 2}" font-size="0.5" text-anchor="start" stroke="none">${label}</text></g>`;
|
|
}
|
|
|
|
/** A break mark (끊음): two short slanted strokes "//" centred on (x, y), so the member is shown as cut. */
|
|
function breakMark(x: number, y: number, halfWidth: number): string {
|
|
const d = 0.12;
|
|
return `<g stroke="#55606e" stroke-width="0.03" fill="none"><line x1="${x - halfWidth - d / 2}" y1="${y - 0.15}" x2="${x - halfWidth + d / 2}" y2="${y + 0.15}"/><line x1="${x + halfWidth - d / 2}" y1="${y - 0.15}" x2="${x + halfWidth + d / 2}" y2="${y + 0.15}"/></g>`;
|
|
}
|
|
|
|
function svgOf(x0: number, y0: number, w: number, h: number, label: string, body: string): string {
|
|
const pw = Math.round(w * FOOTING_PX_PER_M);
|
|
const ph = Math.round(h * FOOTING_PX_PER_M);
|
|
return `<svg viewBox="${x0} ${y0} ${w} ${h}" width="${pw}" height="${ph}" role="img" aria-label="${label}">${body}</svg>`;
|
|
}
|
|
|
|
/** Distinct values (to 1 mm), sorted ascending. */
|
|
function uniq(values: number[]): number[] {
|
|
return [...new Set(values.map((v) => Math.round(v * 1000) / 1000))].sort((a, b) => a - b);
|
|
}
|
|
|
|
/**
|
|
* A chain of dimensions between the footing edges and the pile lines: from the edge to the first pile, between piles, and to the
|
|
* far edge. Horizontal when `horizontal` (at height y), otherwise vertical (at x = y argument).
|
|
*/
|
|
function chain(lines: number[], edgeA: number, edgeB: number, at: number, horizontal: boolean): string {
|
|
const pts = [edgeA, ...lines, edgeB];
|
|
return pts
|
|
.slice(0, -1)
|
|
.map((a, i) => {
|
|
const b = pts[i + 1];
|
|
const label = mm(Math.abs(b - a)).toString();
|
|
return horizontal ? dimH(a, b, at, label) : dimV(at, a, b, label);
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
/** Plan view: the footing rectangle (length along the 교축, width across), the pile circles, the column circle, the axis. */
|
|
export function footingPlanSvg(model: PierModel): string {
|
|
const f = model.footings[0];
|
|
if (!f) return '';
|
|
const pad = 1.5;
|
|
const x0 = f.s - f.length / 2 - pad;
|
|
const y0 = -f.width / 2 - pad;
|
|
const w = f.length + 2 * pad;
|
|
const h = f.width + 2 * pad;
|
|
const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6);
|
|
const parts: string[] = [];
|
|
parts.push(`<rect x="${f.s - f.length / 2}" y="${-f.width / 2}" width="${f.length}" height="${f.width}" fill="#dfe5ec" stroke="#5b6676" stroke-width="0.02"/>`);
|
|
for (const p of piles) {
|
|
parts.push(`<circle cx="${p.s}" cy="${p.n}" r="${p.diameter / 2}" fill="#8898aa" stroke="#3b4656" stroke-width="0.01"/>`);
|
|
}
|
|
for (const c of model.columns) {
|
|
parts.push(`<circle cx="${c.s}" cy="0" r="${c.diameter / 2}" fill="none" stroke="#0f5c66" stroke-width="0.03" stroke-dasharray="0.1 0.06"/>`);
|
|
}
|
|
parts.push(`<line x1="${x0}" y1="0" x2="${x0 + w}" y2="0" stroke="#b4531f" stroke-width="0.02" stroke-dasharray="0.3 0.1 0.05 0.1"/>`);
|
|
// Overall dimensions (전체 길이, 폭) and the pile chains (끝단 거리와 말뚝 간격), as in the 삽도 (승인 2026-10-23).
|
|
const left = f.s - f.length / 2;
|
|
const right = f.s + f.length / 2;
|
|
const top = -f.width / 2;
|
|
const bottom = f.width / 2;
|
|
parts.push(dimH(left, right, top - 0.9, String(mm(f.length))));
|
|
parts.push(dimV(right + 0.9, top, bottom, String(mm(f.width))));
|
|
const xs = uniq(piles.map((p) => p.s));
|
|
const ys = uniq(piles.map((p) => p.n));
|
|
parts.push(chain(xs, left, right, bottom + 0.8, true));
|
|
parts.push(chain(ys, top, bottom, left - 0.8, false));
|
|
return svgOf(x0, y0, w, h, '확대기초·말뚝 평면', parts.join(''));
|
|
}
|
|
|
|
/** Side view (elevation, EL up): the footing, the piles, and the column stub above the footing, both broken (끊음). */
|
|
export function footingSideSvg(model: PierModel): string {
|
|
const f = model.footings[0];
|
|
if (!f) return '';
|
|
const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6);
|
|
const pileTop = piles.length ? Math.max(...piles.map((p) => p.top)) : f.bottom;
|
|
const pileLen = piles.length ? piles[0].top - piles[0].bottom : 0;
|
|
const pad = 1.5;
|
|
const x0 = f.s - f.length / 2 - pad;
|
|
const w = f.length + 2 * pad;
|
|
// Visible band in EL: from the column stub above the footing top, down to the pile stub below the footing bottom.
|
|
const topEl = f.top + COLUMN_STUB;
|
|
const botEl = f.bottom - PILE_STUB;
|
|
const y0 = -topEl;
|
|
const h = topEl - botEl;
|
|
const parts: string[] = [];
|
|
parts.push(`<rect x="${f.s - f.length / 2}" y="${-f.top}" width="${f.length}" height="${f.top - f.bottom}" fill="#dfe5ec" stroke="#5b6676" stroke-width="0.02"/>`);
|
|
for (const p of piles) {
|
|
const from = Math.max(p.bottom, botEl);
|
|
const to = Math.min(p.top, f.bottom);
|
|
if (to > from) parts.push(`<line x1="${p.s}" y1="${-to}" x2="${p.s}" y2="${-from}" stroke="#8898aa" stroke-width="${Math.max(p.diameter, 0.02)}"/>`);
|
|
}
|
|
const col = model.columns[0];
|
|
if (col) {
|
|
parts.push(`<rect x="${col.s - col.diameter / 2}" y="${-f.top - COLUMN_STUB}" width="${col.diameter}" height="${COLUMN_STUB}" fill="#c9d4df" stroke="#0f5c66" stroke-width="0.02"/>`);
|
|
// Break mark on the column stub, at its middle.
|
|
parts.push(breakMark(col.s, -f.top - COLUMN_STUB / 2, col.diameter / 2 + 0.15));
|
|
}
|
|
if (piles.length) {
|
|
// Break mark across the pile group, in the lower stub (the pile label sits below it).
|
|
parts.push(breakMark(f.s, -(botEl + 0.6), f.length / 2));
|
|
}
|
|
if (piles.length) {
|
|
parts.push(`<text x="${f.s}" y="${-(botEl + 1.3)}" font-size="0.5" text-anchor="middle">말뚝 ${mm(pileLen)} (끊음)</text>`);
|
|
}
|
|
// Dimension lines (치수선): footing length above, footing height at the right, the visible pile length at the left.
|
|
parts.push(dimH(f.s - f.length / 2, f.s + f.length / 2, -(f.top + 0.8), `${mm(f.length)}`));
|
|
parts.push(dimV(f.s + f.length / 2 + 0.5, -f.top, -f.bottom, `h1 ${mm(f.top - f.bottom)}`));
|
|
if (piles.length) {
|
|
parts.push(dimV(f.s - f.length / 2 - 0.5, -f.bottom, -(botEl + 0.6), `${mm(pileLen)} (끊음)`));
|
|
}
|
|
void pileTop;
|
|
return svgOf(x0, y0, w, h, '확대기초·말뚝 측면', parts.join(''));
|
|
}
|