feat(core,view): per-row pile counts 열별 본수 (plan C4)
- A 열 is one row across the footing width; each has its own count along the length. Default floor((L − 1400) / 2250) + 1 per 열, as in spec §4.6. - Counts are validated: one per 열, whole numbers 2–30. Spacing below the 2250 mm minimum is shown as a warning, not refused (the plan's 6본 → 5@1800 example). - PierInput gains an optional pileRows; absent means the defaults, so existing files and the P1 results are unchanged. - Change tracking compares arrays by value; 열별 본수 can be reverted and copied (copy item off by default). - The footing card has −/+ per 열 with spacing, total, and the display string. - Tests: defaults 4열 × 5본 = 20 and the 1단계 text; 6본 → 5@1800; limits; model-level 21 piles. - Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b162c2441
commit
17b5b24682
9 files changed
+256
-39
No files matched your search
+54
-5
@@ -6,8 +6,10 @@ import { parseWorkbook, type SheetReport } from './core/parseWorkbook';
|
||||
import { buildPier, type PierModel } from './core/geometry';
|
||||
import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate';
|
||||
import { buildTemplateWorkbook } from './core/template';
|
||||
import { EDITABLE_FIELDS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes';
|
||||
import { EDITABLE_FIELDS, FIELD_LABELS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes';
|
||||
import { adjustRow, layoutText, resolveRowCounts, rowLayout, totalPiles } from './core/piles';
|
||||
import { applyCopy, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy';
|
||||
import { pileGeometry } from './core/geometry';
|
||||
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';
|
||||
@@ -445,7 +447,7 @@ function renderCopyPreview(): void {
|
||||
for (const t of selected) {
|
||||
if (!src) break;
|
||||
const plan = planCopy(src.draft, t.draft, items);
|
||||
const labels = plan.changes.map((c) => `${EDITABLE_FIELDS.find((f) => f.key === c.key)!.label} ${c.from} → ${c.to}`);
|
||||
const labels = plan.changes.map((c) => `${FIELD_LABELS[c.key]} ${c.from} → ${c.to}`);
|
||||
const skipped = plan.skipped.map((k) => k.reason);
|
||||
skippedTotal += skipped.length;
|
||||
if (plan.changes.length) applied++;
|
||||
@@ -508,7 +510,7 @@ function pendingTotal(): number {
|
||||
}
|
||||
|
||||
function fieldLabel(key: EditableKey): string {
|
||||
return EDITABLE_FIELDS.find((f) => f.key === key)!.label;
|
||||
return FIELD_LABELS[key];
|
||||
}
|
||||
|
||||
function editField(name: string, key: EditableKey, raw: string): void {
|
||||
@@ -590,6 +592,53 @@ function fieldRows(box: HTMLDivElement, pier: Pier, keys: EditableKey[]): { over
|
||||
return { overrides, pending };
|
||||
}
|
||||
|
||||
/** Footing length of the generated model, which the 열별 본수 limits depend on. */
|
||||
function footingLengthOf(p: Pier): number | undefined {
|
||||
return p.model?.footings[0]?.length;
|
||||
}
|
||||
|
||||
/** 열별 본수 −/+ per 열 (spec §5.4). Shows the spacing, the total, and a warning below the minimum spacing. */
|
||||
function renderPileRows(box: HTMLDivElement, pier: Pier): void {
|
||||
const footingLength = footingLengthOf(pier);
|
||||
if (pier.input.foundationType !== '말뚝기초' || footingLength === undefined) return;
|
||||
const g = pileGeometry(footingLength);
|
||||
let counts: number[];
|
||||
try {
|
||||
counts = resolveRowCounts(pier.draft.pileRows, g);
|
||||
} catch (err) {
|
||||
box.appendChild(el('p', (err as Error).message, 'warn-text'));
|
||||
return;
|
||||
}
|
||||
const layout = rowLayout(counts, g);
|
||||
const wrap = el('div', undefined, 'pile-rows');
|
||||
wrap.appendChild(el('p', `열별 본수 · 합계 ${totalPiles(counts)}본`, 'lbl'));
|
||||
counts.forEach((count, j) => {
|
||||
const row = el('div', undefined, layout[j].belowMinimum ? 'pile-row warn' : 'pile-row');
|
||||
const minus = el('button', '−', 'btn small');
|
||||
minus.type = 'button';
|
||||
minus.setAttribute('aria-label', `열 ${j + 1} 본수 줄이기`);
|
||||
minus.addEventListener('click', () => setPileRow(pier.input.name, j, -1));
|
||||
const plus = el('button', '+', 'btn small');
|
||||
plus.type = 'button';
|
||||
plus.setAttribute('aria-label', `열 ${j + 1} 본수 늘리기`);
|
||||
plus.addEventListener('click', () => setPileRow(pier.input.name, j, 1));
|
||||
row.append(el('span', `열 ${j + 1}`), minus, el('span', `${count}본`, 'num'), plus, el('span', `간격 ${Math.round(layout[j].spacing * 1000)} mm`, 'num'));
|
||||
if (layout[j].belowMinimum) row.append(el('span', `최소 ${Math.round(g.minSpacing * 1000)} mm보다 좁습니다`, 'warn-text'));
|
||||
wrap.appendChild(row);
|
||||
});
|
||||
wrap.appendChild(el('p', layoutText(counts, g), 'num'));
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
|
||||
function setPileRow(name: string, row: number, delta: number): void {
|
||||
const p = state.piers.find((q) => q.input.name === name);
|
||||
const length = p && footingLengthOf(p);
|
||||
if (!p || length === undefined) return;
|
||||
const counts = resolveRowCounts(p.draft.pileRows, pileGeometry(length));
|
||||
p.draft = { ...p.draft, pileRows: adjustRow(counts, row, delta) };
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** 형상 선택칩 (plan.md C5): the default is pressed; every other option is shown and disabled with the reason. */
|
||||
function renderShapeOptions(member: MemberId): void {
|
||||
const box = $<HTMLDivElement>('member-options');
|
||||
@@ -659,6 +708,7 @@ function renderPlan(): void {
|
||||
regen.disabled = true;
|
||||
} else {
|
||||
fieldRows(fields, pier, member.keys);
|
||||
if (member.id === 'footing') renderPileRows(fields, pier);
|
||||
regen.textContent = `다시 생성 · 변경 ${pendingKeys(pier).length}`;
|
||||
regen.disabled = pendingKeys(pier).length === 0;
|
||||
}
|
||||
@@ -667,8 +717,7 @@ function renderPlan(): void {
|
||||
changes.replaceChildren();
|
||||
const pending = pier ? pendingKeys(pier) : [];
|
||||
for (const key of pier ? overrideKeys(pier) : []) {
|
||||
const f = EDITABLE_FIELDS.find((x) => x.key === key)!;
|
||||
const li = el('li', `${f.label}: 기본 ${pier!.defaults[key]} → 현재 ${pier!.draft[key]}${pending.includes(key) ? ' (미반영)' : ''}`);
|
||||
const li = el('li', `${FIELD_LABELS[key]}: 기본 ${pier!.defaults[key] ?? '없음'} → 현재 ${pier!.draft[key] ?? '없음'}${pending.includes(key) ? ' (미반영)' : ''}`);
|
||||
const revert = el('button', '되돌리기', 'btn small');
|
||||
revert.type = 'button';
|
||||
revert.addEventListener('click', () => revertField(pier!.input.name, key));
|
||||
|
||||
Reference in new issue
Block a user