feat(rebar): 정면 dimension chains computed from the 3D bar positions (기둥 중심 = 0)
- chainDims groups the repeated gaps (n@pitch = total) and writes a single gap as its length. - Top chain: the 전단·띠 stations (C9 @250, C10 @300) from the coping ends. - Lower chain: the upper C14 bars measured from the column centre (0), so the 10@1000 span starts at the column centre. - Stations are the lowest s of their pieces; pieces within 160 mm are one station. - P1 gives the 시안 values: 300 · 18@250=4500 · 8@300=2400 · 18@250=4500 · 300 and 900 · 10@1000=10000 · 900. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0d46220fd
commit
a18a15db7b
3 files changed
+72
-32
No files matched your search
+1
-1
@@ -1092,7 +1092,7 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(row);
|
||||
const fig = el('figure', undefined, 'ground-fig');
|
||||
if (copingView === 'front') {
|
||||
fig.innerHTML = copingFrontSvg(rebar3d.records, L * 1000);
|
||||
fig.innerHTML = copingFrontSvg(rebar3d.records, L * 1000, pier.model.columns[0]?.s ?? 0);
|
||||
fig.appendChild(el('figcaption', '정면 · 교축 방향에서 본 코핑 배근 (3D 철근과 같은 데이터)', 'muted small'));
|
||||
} else {
|
||||
const at = copingStation ?? L / 2;
|
||||
|
||||
@@ -31,17 +31,48 @@ const CALLOUTS: { label: string; names: string[]; where: 'left-top' | 'left' | '
|
||||
{ label: 'C16', names: ['C16'], where: 'top' },
|
||||
];
|
||||
|
||||
/** The 시안 길이 치수 along the top (mm); the parts sum to the coping length for P1 (300 + 4500 + 2400 + 4500 + 300 = 12000). */
|
||||
const TOP_DIMS: { text: string; len: number }[] = [
|
||||
{ text: '300', len: 300 },
|
||||
{ text: '18@250=4500', len: 4500 },
|
||||
{ text: '8@300=2400', len: 2400 },
|
||||
{ text: '18@250=4500', len: 4500 },
|
||||
{ text: '300', len: 300 },
|
||||
];
|
||||
/**
|
||||
* A dimension chain from bar positions (승인 2026-10-26): the gaps between the sorted positions, grouped where the gap
|
||||
* repeats (n@pitch = total). A gap that does not repeat is written as its own length. All in mm.
|
||||
*/
|
||||
export function chainDims(positions: number[], startMm: number, endMm: number): { text: string; len: number; from: number; to: number }[] {
|
||||
const pts = [...new Set([startMm, ...positions.map((p) => Math.round(p)), endMm])].sort((a, b) => a - b);
|
||||
const gaps: { from: number; to: number; len: number }[] = [];
|
||||
for (let i = 0; i + 1 < pts.length; i++) gaps.push({ from: pts[i], to: pts[i + 1], len: pts[i + 1] - pts[i] });
|
||||
const out: { text: string; len: number; from: number; to: number }[] = [];
|
||||
let i = 0;
|
||||
while (i < gaps.length) {
|
||||
let j = i;
|
||||
while (j + 1 < gaps.length && Math.abs(gaps[j + 1].len - gaps[i].len) <= 1) j++;
|
||||
const n = j - i + 1;
|
||||
const total = gaps.slice(i, j + 1).reduce((s, g) => s + g.len, 0);
|
||||
const pitch = Math.round(gaps[i].len);
|
||||
out.push({ text: n === 1 ? `${pitch}` : `${n}@${pitch}=${total}`, len: total, from: gaps[i].from, to: gaps[j].to });
|
||||
i = j + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar positions (mm, along the coping): one per bar, its mean s. A loop or a cross bar sits at one station, so its
|
||||
* points share the same s apart from the bends; the bends are not positions of their own.
|
||||
*/
|
||||
function positionsOf(bars: BarRecord[]): number[] {
|
||||
// The pieces of one station are up to ~130 mm long, so the positions within 160 mm of each other are one station
|
||||
// (the closest real spacing in the chains is 250 mm).
|
||||
const vals = bars.map((r) => (r.pts.reduce((s, p) => s + p[0], 0) / r.pts.length) * 1000).sort((x, y) => x - y);
|
||||
const groups: number[][] = [];
|
||||
for (const v of vals) {
|
||||
const g = groups[groups.length - 1];
|
||||
if (g && v - g[0] <= 160) g.push(v);
|
||||
else groups.push([v]);
|
||||
}
|
||||
// The station is the lowest s of its pieces (the stirrup starts there, its pieces run on from it).
|
||||
return groups.map((g) => Math.round(g[0]));
|
||||
}
|
||||
|
||||
/** 정면: the bars seen from the 교축 direction, with the bar-group bubbles and the length dimensions (시안 ③). */
|
||||
export function copingFrontSvg(records: BarRecord[], lengthMm: number): string {
|
||||
export function copingFrontSvg(records: BarRecord[], lengthMm: number, columnSm = 0): string {
|
||||
const bars = records.filter(isCoping);
|
||||
const els = bars.flatMap((r) => r.pts.map((p) => p[1]));
|
||||
const top = Math.max(...els);
|
||||
@@ -62,30 +93,24 @@ export function copingFrontSvg(records: BarRecord[], lengthMm: number): string {
|
||||
parts.push(`<polyline points="${pts}" fill="none" stroke="${COLOR[copingBarGroup(r.name)]}" stroke-width="10"></polyline>`);
|
||||
}
|
||||
|
||||
// Top dimension line: the parts of the 시안, only when they add up to the coping length.
|
||||
const dimTotal = TOP_DIMS.reduce((s, d) => s + d.len, 0);
|
||||
// First chain (the top line): the 전단·띠 positions of C9 (@250) and C10 (@300) along the coping, ends to the bar extents.
|
||||
const loops = bars.filter((r) => /^C(9|10|11)$/.test(r.name));
|
||||
const topChain = chainDims(positionsOf(loops), 0, lengthMm);
|
||||
const dimY = minY - 1100;
|
||||
if (Math.abs(dimTotal - lengthMm) < 1) {
|
||||
let x = minX;
|
||||
for (const d of TOP_DIMS) {
|
||||
const x2 = x + d.len;
|
||||
parts.push(`<line x1="${x}" y1="${dimY}" x2="${x2}" y2="${dimY}" stroke="#18212B" stroke-width="10"></line><text x="${(x + x2) / 2}" y="${dimY - 80}" text-anchor="middle" font-size="150" fill="#18212B">${d.text}</text>`);
|
||||
x = x2;
|
||||
}
|
||||
for (const d of topChain) {
|
||||
parts.push(`<line x1="${d.from}" y1="${dimY}" x2="${d.to}" y2="${dimY}" stroke="#18212B" stroke-width="10"></line><text x="${(d.from + d.to) / 2}" y="${dimY - 80}" text-anchor="middle" font-size="150" fill="#18212B">${d.text}</text>`);
|
||||
}
|
||||
// Second line (시안): the column-centre span 900 · 10@1000=10000 · 900, from the design values.
|
||||
// Second chain (the lower line): the upper C14 bars, measured from the column centre (0).
|
||||
const c14 = bars.filter((r) => r.name === 'C14');
|
||||
const c14Top = Math.max(...c14.flatMap((r) => r.pts.map((p) => p[1])));
|
||||
const c14Upper = c14.filter((r) => r.pts.every((p) => p[1] >= c14Top - 0.3));
|
||||
const col = columnSm * 1000;
|
||||
const lowerChain = chainDims(positionsOf(c14Upper).map((v) => v - col), minX - col, maxX - col);
|
||||
const dimY2 = dimY - 600;
|
||||
const LOWER: { text: string; len: number }[] = [
|
||||
{ text: '900', len: 900 },
|
||||
{ text: '10@1000=10000 (기둥 중심부터)', len: 10000 },
|
||||
{ text: '900', len: 900 },
|
||||
];
|
||||
let x2 = minX;
|
||||
for (const d of LOWER) {
|
||||
const xe = x2 + d.len;
|
||||
parts.push(`<line x1="${x2}" y1="${dimY2}" x2="${xe}" y2="${dimY2}" stroke="#18212B" stroke-width="10"></line><text x="${(x2 + xe) / 2}" y="${dimY2 - 80}" text-anchor="middle" font-size="150" fill="#18212B">${d.text}</text>`);
|
||||
x2 = xe;
|
||||
for (const d of lowerChain) {
|
||||
parts.push(`<line x1="${d.from}" y1="${dimY2}" x2="${d.to}" y2="${dimY2}" stroke="#18212B" stroke-width="10"></line><text x="${(d.from + d.to) / 2}" y="${dimY2 - 80}" text-anchor="middle" font-size="150" fill="#18212B">${d.text}</text>`);
|
||||
}
|
||||
parts.push(`<line x1="0" y1="${dimY2 - 260}" x2="0" y2="${dimY2 + 120}" stroke="#B4531F" stroke-width="10"></line><text x="0" y="${dimY2 + 260}" text-anchor="middle" font-size="150" fill="#B4531F">기둥 중심</text>`);
|
||||
parts.push(`<text x="${midX}" y="${minY - 1500}" text-anchor="middle" font-size="200" fill="#18212B">코핑 길이 ${lengthMm}</text>`);
|
||||
|
||||
// Bubbles: one per bar group, a leader from the bubble to the middle of the group's bars.
|
||||
|
||||
Reference in new issue
Block a user