From 553c69663894d51a21ef66c42fa48324b78f1a09 Mon Sep 17 00:00:00 2001 From: ypshim Date: Fri, 23 Oct 2026 14:12:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(view):=20bearing=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20inputs=20(mortar=20h2,=20bearing=20h3,=20s?= =?UTF-8?q?olplate=20T,=20margin)=20and=20recalculated=20E,=20L,=20W,=20T1?= =?UTF-8?q?,=20T2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the bearing card, four inputs drive E = h1 + h2 + h3 + h4, L = A + margin, W = B + margin and T1/T2 = T ± L·|S|/2. The defaults are the drawing values, so every row gives back the drawing values; changed rows are marked. The values are screen state for now and do not change the model or the project file (Q16 stays open). Co-Authored-By: Claude Haiku 5.5 --- src/core/bearing-ref.ts | 22 +++++++++++++++++ src/main.ts | 52 ++++++++++++++++++++++++++++++++++++++- src/style.css | 1 + tests/bearing-ref.test.ts | 23 +++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/core/bearing-ref.ts b/src/core/bearing-ref.ts index 1b70e5e..ace536b 100644 --- a/src/core/bearing-ref.ts +++ b/src/core/bearing-ref.ts @@ -79,3 +79,25 @@ export function solplateTs(T: number, L: number, s: number): { T1: number; T2: n const half = (L * Math.abs(s)) / 200; return { T1: Math.round(T + half), T2: Math.round(T - half) }; } + +/** 공통 규칙 (2단계, 승인 2026-10-23): inputs that drive E, L, W, T1, T2. Defaults are the drawing's values. */ +export interface BearingRules { + /** 모르타르 h2 (mm). */ + h2: number; + /** 받침 높이 h3 (mm). */ + h3: number; + /** 솔플레이트 중심 두께 T (mm). */ + T: number; + /** 솔플 여유 (mm): L = A + margin, W = B + margin. */ + margin: number; +} + +export const BEARING_RULES_DEFAULT: BearingRules = { h2: 50, h3: 181, T: SOLPLATE_T_RULE, margin: 100 }; + +/** Values of one bearing with the 공통 규칙 applied: E = h1 + h2 + h3 + h4, L = A + margin, W = B + margin, T1/T2 from T. */ +export function applyBearingRules(row: BearingRefRow, rules: BearingRules): { E: number; L: number; W: number; T1: number; T2: number } { + const sp = SOLPLATE_REF[row.no - 1]; + const L = row.A + rules.margin; + 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 }; +} diff --git a/src/main.ts b/src/main.ts index 427d232..7b7d5a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,10 @@ 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 { BEARING_ELEVATION_REF, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } from './core/bearing-ref'; +import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingRules, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } 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 }; 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'; @@ -1251,6 +1254,53 @@ function setMaterial(key: 'fck' | GradeKey, raw: string): void { function renderBearingRef(box: HTMLDivElement, pier: Pier): void { const m = pier.model; const wrap = el('div', undefined, 'bearing-ref'); + 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'); + for (const [key, label] of rules) { + const id = `bear-${key}`; + const lab = el('label', label); + lab.htmlFor = id; + const input = el('input'); + input.id = id; + input.type = 'number'; + input.step = 'any'; + input.value = String(bearingRules[key]); + const changed = bearingRules[key] !== BEARING_RULES_DEFAULT[key]; + if (changed) input.classList.add('changed-input'); + input.addEventListener('change', () => { + const v = Number(input.value); + const min = key === 'margin' ? 0 : 0.0001; + if (!Number.isFinite(v) || v < min) { + setError(`${label}은(는) ${key === 'margin' ? '0 이상' : '0보다 큰'} 숫자여야 합니다.`); + renderAll(); + return; + } + bearingRules = { ...bearingRules, [key]: v }; + renderAll(); + }); + grid.append(lab, input, el('span', `기본 ${BEARING_RULES_DEFAULT[key]}`, 'muted small')); + } + wrap.appendChild(grid); + wrap.appendChild(el('p', '규칙 적용 값 (E = h1 + h2 + h3 + h4, L = A + 여유, W = B + 여유, T1·T2 = T ± L·|S|/2)', 'lbl')); + const ct = el('table', undefined, 'grid-table rebar-table'); + const ch = el('thead'); + const chr = el('tr'); + for (const h of ['No', '열', 'E', 'L', 'W', 'T1 (−측)', 'T2 (+측)', '도면 대비']) chr.appendChild(el('th', h)); + ch.appendChild(chr); + ct.appendChild(ch); + const cb = el('tbody'); + for (const r of BEARING_ELEVATION_REF) { + const v = applyBearingRules(r, bearingRules); + const d = applyBearingRules(r, BEARING_RULES_DEFAULT); + const same = v.E === d.E && v.L === d.L && v.W === d.W && v.T1 === d.T1 && v.T2 === d.T2; + cb.appendChild(el('tr')).append( + el('td', String(r.no)), el('td', r.row), el('td', String(v.E)), el('td', String(v.L)), el('td', String(v.W)), + el('td', String(v.T1)), el('td', String(v.T2)), el('td', same ? '같음' : '규칙 변경'), + ); + } + 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', '도면값은 모델에 쓰지 않습니다(Q16). 아래 "차이"는 도면과 모델의 코핑 상면 차이입니다.', 'muted small')); const front = m?.segments ?? []; diff --git a/src/style.css b/src/style.css index a586984..516d0ce 100644 --- a/src/style.css +++ b/src/style.css @@ -793,3 +793,4 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; /* ② 교량받침 reference tables (읽기 전용) */ .bearing-ref .table-scroll { overflow-x: auto; margin-bottom: 8px; } .bearing-ref table { font-size: 12px; } +.bearing-ref input.changed-input { border: 2px solid #e8833a; background: #fff6ee; } diff --git a/tests/bearing-ref.test.ts b/tests/bearing-ref.test.ts index 58fe925..0f52a7d 100644 --- a/tests/bearing-ref.test.ts +++ b/tests/bearing-ref.test.ts @@ -41,3 +41,26 @@ describe('교량받침 reference (도면값, 읽기 전용, 1단계)', () => { for (const d of diffs) expect(d).toBeCloseTo(0.211, 2); }); }); + +import { applyBearingRules, BEARING_RULES_DEFAULT } from '../src/core/bearing-ref'; + +describe('교량받침 공통 규칙 (2단계)', () => { + it('with the drawing values, the rules give back the drawing E, L, W, T1, T2 on every bearing', () => { + BEARING_ELEVATION_REF.forEach((r, i) => { + const v = applyBearingRules(r, BEARING_RULES_DEFAULT); + expect(v.E, `No ${r.no}`).toBe(r.E); + expect(v.L, `No ${r.no}`).toBe(SOLPLATE_REF[i].L); + expect(v.W, `No ${r.no}`).toBe(SOLPLATE_REF[i].W); + expect(v.T1, `No ${r.no}`).toBe(SOLPLATE_REF[i].T1); + expect(v.T2, `No ${r.no}`).toBe(SOLPLATE_REF[i].T2); + }); + }); + + it('a 10 mm larger mortar h2 raises E by 10 and leaves L, W and T1, T2 unchanged', () => { + const r = BEARING_ELEVATION_REF[0]; + const v = applyBearingRules(r, { ...BEARING_RULES_DEFAULT, h2: BEARING_RULES_DEFAULT.h2 + 10 }); + expect(v.E).toBe(r.E + 10); + expect(v.L).toBe(r.A + 100); + expect(v.T1).toBe(SOLPLATE_REF[0].T1); + }); +});