feat(rebar,view): 받침 기준 — project-wide choice between Excel z (default) and drawing EL.X, with per-bearing drawing input on ②
The project has one basis: 엑셀 z (default) or 도면 EL.X. On ②, the bearing card lists EL.X, h1 and h2 for each bearing (per pier), with a button to fill the reference values. With the drawing basis the model takes the drawing EL.X as the bearing height; the coping tops stay within the 5 mm tolerance, the front block is 164 mm and the back 139 mm (excluding the pad). The check compares EL.X − h1 − h2 with the Excel-basis coping top. The basis and the drawing values are saved in the project file (optional fields). Changes regenerate every pier at once. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
802a1eab97
commit
e4e141c801
4 files changed
+197
-12
No files matched your search
@@ -1,3 +1,6 @@
|
||||
import type { PierInput } from './types';
|
||||
import type { PierModel } from './geometry';
|
||||
|
||||
/**
|
||||
* 교량받침 reference tables from the P1 상세도 (도면값, 읽기 전용, 2026-10-23). They are not used in the model:
|
||||
* the model keeps the imported values (Q16). EL.X is the drawing's bearing level; E = h1 + h2 + h3 + h4.
|
||||
@@ -101,3 +104,41 @@ export function applyBearingRules(row: BearingRefRow, rules: BearingRules): { E:
|
||||
const { T1, T2 } = solplateTs(rules.T, L, sp.s);
|
||||
return { E: row.h1 + rules.h2 + rules.h3 + row.h4, L, W: row.B + rules.margin, T1, T2 };
|
||||
}
|
||||
|
||||
/** 받침 기준 (Q16, 승인 2026-10-23): 'excel' = 엑셀 z (기본), 'drawing' = 도면 EL.X. Project-wide. */
|
||||
export type BearingBasis = 'excel' | 'drawing';
|
||||
|
||||
/** One bearing's drawing values entered on ② (mm for h1, h2; m for EL.X). */
|
||||
export interface DrawingBearing {
|
||||
no: number;
|
||||
elx: number;
|
||||
h1: number;
|
||||
h2: number;
|
||||
}
|
||||
|
||||
/** Tolerance for the coping top check against the drawing (승인 2026-10-23: 5 mm). */
|
||||
export const DRAWING_TOL_MM = 5;
|
||||
|
||||
/** The input with each bearing's z replaced by the drawing EL.X, for bearings that have a drawing value. */
|
||||
export function withDrawingBasis(input: PierInput, drawing: DrawingBearing[]): PierInput {
|
||||
const byNo = new Map(drawing.map((d) => [d.no, d]));
|
||||
return {
|
||||
...input,
|
||||
bearings: input.bearings.map((b) => {
|
||||
const d = byNo.get(b.no);
|
||||
return d ? { ...b, z: d.elx } : b;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Coping top implied by each drawing row (EL.X − h1 − h2) against the model's coping top; differences in mm. */
|
||||
export function drawingCopingCheck(model: PierModel, drawing: DrawingBearing[]): { no: number; diffMm: number; ok: boolean }[] {
|
||||
const front = model.blocks.filter((b) => b.row === '전열');
|
||||
const back = model.blocks.filter((b) => b.row === '후열');
|
||||
return drawing.map((d) => {
|
||||
const implied = d.elx - (d.h1 + d.h2) / 1000;
|
||||
const modelTop = d.no <= 5 ? model.segments[d.no - 1]?.top : back[d.no - 6]?.bottom;
|
||||
const diffMm = modelTop === undefined ? NaN : Math.round((modelTop - implied) * 1000 * 10) / 10;
|
||||
return { no: d.no, diffMm, ok: Number.isFinite(diffMm) && Math.abs(diffMm) <= DRAWING_TOL_MM };
|
||||
});
|
||||
}
|
||||
@@ -31,6 +31,10 @@ export interface ProjectFile {
|
||||
history: { version: string; note: string; at: string }[];
|
||||
/** ③ 공통 설정 (fck · fy). Absent in older files: the defaults are used. */
|
||||
common?: Material;
|
||||
/** 받침 기준 (project-wide): 'excel' (default) or 'drawing'. Absent = 'excel'. */
|
||||
bearingBasis?: 'excel' | 'drawing';
|
||||
/** 도면 받침 값 per pier name (EL.X in m, h1 and h2 in mm). */
|
||||
bearingDrawing?: Record<string, { no: number; elx: number; h1: number; h2: number }[]>;
|
||||
/** ② 부재별 fck (MPa). Absent in older files: every member uses the 공통 fck. */
|
||||
fckByMember?: FckByMember;
|
||||
}
|
||||
|
||||
+117
-12
@@ -16,10 +16,14 @@ import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar
|
||||
import { DEFAULT_MATERIAL, FCK_MEMBERS, fyOf, GRADE_CHOICES, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common';
|
||||
import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity';
|
||||
import { P1_COPING_GEOM } from './core/coping-geom';
|
||||
import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingRules, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } from './core/bearing-ref';
|
||||
import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref';
|
||||
|
||||
/** 공통 규칙 of the bearing card (2단계). Screen state for now; not yet saved to the project file. */
|
||||
let bearingRules: BearingRules = { ...BEARING_RULES_DEFAULT };
|
||||
|
||||
/** 받침 기준 for the project (Q16, 승인 2026-10-23): 'excel' by default. Drawing values per pier name. */
|
||||
let bearingBasis: BearingBasis = 'excel';
|
||||
let bearingDrawing: Record<string, DrawingBearing[]> = {};
|
||||
import { allVisibleChecked, setVisibleChecked, type TypeFilter } from './core/selection';
|
||||
import { choose, defaultSummary, isSelectable, SHAPE_GROUPS, UNDEFINED_REASON, type MemberId } from './core/shape-options';
|
||||
import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project';
|
||||
@@ -199,9 +203,12 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
|
||||
const input = withCopingDefaults(rawInput);
|
||||
const issues: Issue[] = [];
|
||||
let model: PierModel | null = null;
|
||||
// The model uses the drawing EL.X when the project basis is 'drawing' and this pier has drawing values (Q16).
|
||||
const drawing = bearingBasis === 'drawing' ? bearingDrawing[input.name] : undefined;
|
||||
const modelInput = drawing?.length ? withDrawingBasis(input, drawing) : input;
|
||||
if (validateInput(input, issues)) {
|
||||
try {
|
||||
model = buildPier(input);
|
||||
model = buildPier(modelInput);
|
||||
validatePier(input, model, issues);
|
||||
} catch (err) {
|
||||
issues.push({ code: '미실시', level: 'E', pier: input.name, message: (err as Error).message });
|
||||
@@ -1251,9 +1258,102 @@ function setMaterial(key: 'fck' | GradeKey, raw: string): void {
|
||||
}
|
||||
|
||||
/** ② 교량받침 reference (1단계, 읽기 전용): the drawing's ELEVATION and 솔플레이트 tables, not used in the model (Q16). */
|
||||
/** Rebuilds every pier from its draft, so that a change of basis or drawing values reaches the model. */
|
||||
function regenerateAllPiers(): void {
|
||||
state.piers = state.piers.map((p) => regenerateEntry(p));
|
||||
setupScene();
|
||||
}
|
||||
|
||||
/** ② 받침 기준 (Q16, 승인 2026-10-23): project-wide basis, and the drawing EL.X / h1 / h2 for this pier. */
|
||||
function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('p', '받침 기준 (프로젝트 전체 · 기본: 엑셀 z)', 'lbl'));
|
||||
const sel = el('select');
|
||||
sel.id = 'bearing-basis';
|
||||
for (const [value, label] of [['excel', '엑셀 z (기본)'], ['drawing', '도면 EL.X']] as const) {
|
||||
const o = el('option', label);
|
||||
o.value = value;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = bearingBasis;
|
||||
sel.addEventListener('change', () => {
|
||||
bearingBasis = sel.value as BearingBasis;
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
});
|
||||
wrap.appendChild(sel);
|
||||
|
||||
const name = pier.input.name;
|
||||
const current = new Map((bearingDrawing[name] ?? []).map((d) => [d.no, d]));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['No', '열', 'EL.X (m)', 'h1 (mm)', 'h2 (mm)', '코핑 상면 차이 (mm)']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
// The reference is the Excel-basis model: the drawing EL.X − h1 − h2 must reproduce its coping tops within 5 mm.
|
||||
const checks = bearingBasis === 'drawing' && bearingDrawing[name]?.length ? drawingCopingCheck(buildPier(pier.input), bearingDrawing[name]) : [];
|
||||
const checkOf = new Map(checks.map((c) => [c.no, c]));
|
||||
const readTable = (): DrawingBearing[] => {
|
||||
const rows: DrawingBearing[] = [];
|
||||
body.querySelectorAll<HTMLTableRowElement>('tr').forEach((tr) => {
|
||||
const inputs = tr.querySelectorAll<HTMLInputElement>('input');
|
||||
const [elx, h1, h2] = Array.from(inputs).map((i) => Number(i.value));
|
||||
const no = Number(tr.dataset.no);
|
||||
if ([elx, h1, h2].every((v) => Number.isFinite(v) && v > 0)) rows.push({ no, elx, h1, h2 });
|
||||
});
|
||||
return rows;
|
||||
};
|
||||
const commit = () => {
|
||||
bearingDrawing = { ...bearingDrawing, [name]: readTable() };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
};
|
||||
for (const b of [...pier.input.bearings].sort((x, y) => x.no - y.no)) {
|
||||
const d = current.get(b.no);
|
||||
const tr = el('tr');
|
||||
tr.dataset.no = String(b.no);
|
||||
const cells: (string | HTMLElement)[] = [String(b.no), b.row];
|
||||
for (const [value, step] of [[d?.elx, '0.001'], [d?.h1, '1'], [d?.h2, '1']] as const) {
|
||||
const input = el('input');
|
||||
input.type = 'number';
|
||||
input.step = step;
|
||||
input.value = value === undefined ? '' : String(value);
|
||||
input.addEventListener('change', commit);
|
||||
cells.push(input);
|
||||
}
|
||||
const c = checkOf.get(b.no);
|
||||
cells.push(c === undefined ? '—' : `${c.diffMm.toFixed(1)}${c.ok ? '' : ` (허용 ${DRAWING_TOL_MM} 초과)`}`);
|
||||
tr.append(
|
||||
...cells.map((x) => {
|
||||
const td = el('td');
|
||||
if (typeof x === 'string') td.textContent = x;
|
||||
else td.appendChild(x);
|
||||
return td;
|
||||
}),
|
||||
);
|
||||
body.appendChild(tr);
|
||||
}
|
||||
table.appendChild(body);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
|
||||
const fill = el('button', '도면값 채우기 (참조 값)', 'btn small');
|
||||
fill.type = 'button';
|
||||
fill.addEventListener('click', () => {
|
||||
bearingDrawing = { ...bearingDrawing, [name]: BEARING_ELEVATION_REF.map((r) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2 })) };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
});
|
||||
wrap.appendChild(fill);
|
||||
if (bearingBasis === 'drawing' && !bearingDrawing[name]?.length) {
|
||||
wrap.appendChild(el('p', '도면 값이 없어서 엑셀 z로 계산 중입니다.', 'warn-text'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
|
||||
const m = pier.model;
|
||||
const wrap = el('div', undefined, 'bearing-ref');
|
||||
renderBearingBasis(wrap, pier);
|
||||
wrap.appendChild(el('p', '공통 규칙 (입력 · 기본값 = 도면값)', 'lbl'));
|
||||
const rules: [keyof BearingRules, string][] = [['h2', '모르타르 h2 (mm)'], ['h3', '받침 높이 h3 (mm)'], ['T', '솔플레이트 중심 T (mm)'], ['margin', '솔플 여유 (mm)']];
|
||||
const grid = el('div', undefined, 'edit-row');
|
||||
@@ -1302,26 +1402,25 @@ function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
|
||||
ct.appendChild(cb);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(ct);
|
||||
wrap.appendChild(el('p', '교량받침 ELEVATION TABLE (도면값 · 읽기 전용, mm · EL m)', 'lbl'));
|
||||
wrap.appendChild(el('p', 'EL.X = 몰탈 50 mm를 포함한 받침블럭 상면. 도면값은 모델에 쓰지 않으며, 차이는 도면과 모델의 코핑 상면 차이입니다.', 'muted small'));
|
||||
const front = m?.segments ?? [];
|
||||
wrap.appendChild(el('p', '엑셀 z를 EL.X로 씁니다(몰탈 50 mm 포함 받침블럭 상면, Q16 결정 2026-10-23). 도면 EL.X와의 차이는 "도면 ≠ 엑셀"로만 표시합니다.', 'muted small'));
|
||||
const front = m?.blocks.filter((b) => b.row === '전열') ?? [];
|
||||
const backBlocks = m?.blocks.filter((b) => b.row === '후열') ?? [];
|
||||
const et = el('table', undefined, 'grid-table rebar-table');
|
||||
const eh = el('thead');
|
||||
const ehr = el('tr');
|
||||
for (const h of ['No', '열', 'EL.X', 'E', 'A', 'B', 'C1', 'S1', '코핑 상면 = EL.X − h1 − h2', '모델 코핑 상면', '차이 (m)']) ehr.appendChild(el('th', h));
|
||||
for (const h of ['No', '열', '엑셀 z (= EL.X, 모델)', '도면 EL.X', '도면 − 엑셀 (mm)', 'E', 'A', 'B', 'C1', 'S1']) ehr.appendChild(el('th', h));
|
||||
eh.appendChild(ehr);
|
||||
et.appendChild(eh);
|
||||
const eb = el('tbody');
|
||||
for (const r of BEARING_ELEVATION_REF) {
|
||||
// EL.X is the block top including the 50 mm mortar: coping top = EL.X − h1 − h2 (승인 2026-10-23).
|
||||
const drawingTop = r.elx - (r.h1 + r.h2) / 1000;
|
||||
const modelTop = r.row === '전열' ? front[r.no - 1]?.top : backBlocks[r.no - 6]?.bottom;
|
||||
const diff = modelTop === undefined ? NaN : modelTop - drawingTop;
|
||||
// Excel z is EL.X (Q16 decision 2026-10-23, option 1); the drawing's EL.X is shown for comparison only.
|
||||
const block = r.row === '전열' ? front[r.no - 1] : backBlocks[r.no - 6];
|
||||
const excelZ = block?.padTop;
|
||||
const diffMm = excelZ === undefined ? NaN : Math.round((r.elx - excelZ) * 1000);
|
||||
eb.appendChild(el('tr')).append(
|
||||
el('td', String(r.no)), el('td', r.row), el('td', r.elx.toFixed(3)), el('td', String(r.E)),
|
||||
el('td', String(r.no)), el('td', r.row), el('td', excelZ === undefined ? '—' : excelZ.toFixed(3)), el('td', r.elx.toFixed(3)),
|
||||
el('td', Number.isNaN(diffMm) ? '—' : String(diffMm)), el('td', String(r.E)),
|
||||
el('td', String(r.A)), el('td', String(r.B)), el('td', String(r.C1)), el('td', String(r.S1)),
|
||||
el('td', drawingTop.toFixed(3)), el('td', modelTop === undefined ? '—' : modelTop.toFixed(3)),
|
||||
el('td', Number.isNaN(diff) ? '—' : diff.toFixed(3)),
|
||||
);
|
||||
}
|
||||
et.appendChild(eb);
|
||||
@@ -1457,6 +1556,8 @@ function snapshot(): string {
|
||||
piers: state.piers.map((p) => ({ input: p.input, defaults: p.defaults, draft: p.draft })),
|
||||
material: state.material,
|
||||
fckByMember: state.fckByMember,
|
||||
bearingBasis,
|
||||
bearingDrawing,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1473,6 +1574,8 @@ function currentProjectText(): string {
|
||||
history: state.history,
|
||||
common: state.material,
|
||||
fckByMember: state.fckByMember,
|
||||
bearingBasis,
|
||||
bearingDrawing,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1558,6 +1661,8 @@ function openProjectText(text: string, saved: boolean): void {
|
||||
state.history = parsed.history;
|
||||
state.material = materialFrom(parsed.common);
|
||||
state.fckByMember = { ...parsed.fckByMember };
|
||||
bearingBasis = parsed.bearingBasis ?? 'excel';
|
||||
bearingDrawing = Object.fromEntries(Object.entries(parsed.bearingDrawing ?? {}).map(([k, v]) => [k, [...v]]));
|
||||
state.piers = parsed.piers.map((p) => ({
|
||||
...buildEntry(p.input, false),
|
||||
defaults: p.defaults ?? p.input,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { BEARING_ELEVATION_REF, DRAWING_TOL_MM, drawingCopingCheck, withDrawingBasis, type DrawingBearing } from '../src/core/bearing-ref';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
const drawing: DrawingBearing[] = BEARING_ELEVATION_REF.map((r) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2 }));
|
||||
|
||||
describe('받침 기준: 도면 EL.X (Q16 option 2, 승인 2026-10-23)', () => {
|
||||
it('the default basis (엑셀 z) keeps the model as before: the drawing check still reports the EL.X gap', () => {
|
||||
const excel = buildPier(loadP1());
|
||||
expect(excel.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.561, 3);
|
||||
});
|
||||
|
||||
it('with the drawing basis the pads sit at the drawing EL.X and the coping tops stay within the 5 mm tolerance', () => {
|
||||
const m = buildPier(withDrawingBasis(loadP1(), drawing));
|
||||
expect(m.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.625, 3);
|
||||
for (const c of drawingCopingCheck(m, drawing)) {
|
||||
expect(Math.abs(c.diffMm), `No ${c.no}`).toBeLessThanOrEqual(DRAWING_TOL_MM);
|
||||
}
|
||||
});
|
||||
|
||||
it('with the drawing basis the front block (h1) is 164 mm and the back block 139 mm, excluding the 50 mm pad', () => {
|
||||
const m = buildPier(withDrawingBasis(loadP1(), drawing));
|
||||
const front = m.blocks.filter((b) => b.row === '전열')[0];
|
||||
const back = m.blocks.filter((b) => b.row === '후열')[0];
|
||||
expect(Math.round((front.top - front.bottom) * 1000)).toBe(164);
|
||||
expect(Math.round((back.top - back.bottom) * 1000)).toBe(139);
|
||||
});
|
||||
|
||||
it('a drawing row that is off by more than 5 mm is flagged', () => {
|
||||
const bad = drawing.map((d) => (d.no === 3 ? { ...d, elx: d.elx + 0.02 } : d));
|
||||
const c = drawingCopingCheck(buildPier(loadP1()), bad).find((x) => x.no === 3)!;
|
||||
expect(c.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user