Merge origin/master with current Pierzainer development
Preserve local abutment, cast-in-place rebar and compact header features alongside remote drawing, ground-link and quantity workspaces. Adapt drawing and takeoff consumers to generated solplate plans, retain current P1 regression coverage, and resolve bearing-detail annotation collisions. Validation: 122 test files / 975 tests passed; production build passed; 10 browser integration checks passed.
This commit is contained in:
commit
f51865c57b
2645 files changed
+64918
-4910
No files matched your search
@@ -50,11 +50,12 @@ export interface ColumnInput {
|
||||
* the other values stay as in COLUMN_P1.
|
||||
*/
|
||||
export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1, columnIndex = 0): ColumnInput {
|
||||
if (!model.footings.length) throw new Error('현장타설말뚝 기둥은 전용 배근 입력을 사용합니다. 확대기초 정착 규칙은 적용하지 않습니다.');
|
||||
// No footing (현장타설말뚝, merged 2026-11-06 with origin's guard): Hc still runs to the column bottom (= 지반고) and Hf is 0.
|
||||
// The cast-in-place rebar itself comes from cast-layout.ts; callers only use the footing rules when a footing exists.
|
||||
const col = model.columns[columnIndex];
|
||||
const globalS = col.s + model.stretches[col.k].base;
|
||||
const seg = model.segments.find((g) => globalS >= g.s0 - 1e-9 && globalS <= g.s1 + 1e-9) ?? model.segments[0];
|
||||
const footing = model.footings.find(f => f.k === col.k && Math.abs(f.s - col.s) <= f.length / 2)!;
|
||||
const footing = model.footings.find(f => f.k === col.k && Math.abs(f.s - col.s) <= f.length / 2);
|
||||
const Hc = Math.round((col.top - col.bottom) * 1000 * 10) / 10;
|
||||
const hcop = Math.round((seg.top - col.top) * 1000 * 10) / 10;
|
||||
// C3 (상면 3단) = the lowest coping top −127 (C1 cover) −2·s_lay (상면 주철근 단 간격 input); P1 was 43.170 at 57.2.
|
||||
@@ -64,7 +65,7 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1,
|
||||
// e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23).
|
||||
const eTop = Math.round((seg.top - (col.top + seg.top) / 2) * 10000) / 10;
|
||||
return { ...base, ...model.columnRebar, footing: model.footingRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm,
|
||||
Hf: (footing.top - footing.bottom) * 1000, bothEndZones: model.columns.length > 1 };
|
||||
Hf: footing ? (footing.top - footing.bottom) * 1000 : 0, bothEndZones: model.columns.length > 1 };
|
||||
}
|
||||
|
||||
export const COLUMN_P1: ColumnInput = {
|
||||
|
||||
+36
-26
@@ -1,4 +1,6 @@
|
||||
import './style.css';
|
||||
import { createDrawingWorkspace } from '../도면작성/workspace';
|
||||
import { createTakeoffWorkspace } from '../수량산출/workspace';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { swapXYAzimuth } from './core/coordinate-frame';
|
||||
@@ -145,8 +147,8 @@ interface Pier extends PierValues {
|
||||
}
|
||||
|
||||
/** Part toggles in the order of 시안 ① "부재 표시". Each toggle controls one or more scene parts. */
|
||||
/** The stages: ① 형상 생성, ② 형상계획, ③ 철근배근. 'quantity' is the 철근재료표 screen opened from ③ (시안: header ①②③, 2026-11-06). */
|
||||
type Stage = 'shape' | 'plan' | 'rebar' | 'quantity';
|
||||
/** ① 형상 생성, ② 형상계획, ③ 철근배근, ④ 도면작성 (origin). 'quantity' is the 재료표 · 수량산출 screen opened from ③ (시안: no header step, 2026-11-06). */
|
||||
type Stage = 'shape' | 'plan' | 'rebar' | 'drawing' | 'quantity';
|
||||
|
||||
/** 부재 표시 toggles (2026-11-03). `pier` parts follow the pier scenes; `abut` names follow the abutment groups (abut-scene.ts). */
|
||||
const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
|
||||
@@ -279,6 +281,21 @@ let backupTimer: number | undefined;
|
||||
|
||||
const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T;
|
||||
|
||||
const drawingWorkspace = createDrawingWorkspace($('view-drawing'), () => ({
|
||||
project: state.project, selected: state.active,
|
||||
piers: state.piers.map(p => ({ input: p.input, model: p.model, dirty: pendingKeys(p).length > 0 })),
|
||||
}), selectPier);
|
||||
|
||||
const quantityRebarTon = new Map<string,number|null>();
|
||||
const quantityWorkspace = createTakeoffWorkspace($('view-quantity'), () => ({
|
||||
project: state.project, selected: state.active,
|
||||
piers: state.piers.map(p => ({input:p.input,model:p.model,dirty:pendingKeys(p).length>0,rebarTon:quantityRebarTon.get(p.input.name)})),
|
||||
}), selectPier, (name,data) => {
|
||||
const pier=state.piers.find(p=>p.input.name===name);if(!pier)return;
|
||||
pier.input={...pier.input,quantityTakeoff:data};pier.draft={...pier.draft,quantityTakeoff:data};
|
||||
renderSaveStatus();scheduleBackup();
|
||||
});
|
||||
|
||||
function el<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, cls?: string): HTMLElementTagNameMap[K] {
|
||||
const node = document.createElement(tag);
|
||||
if (text !== undefined) node.textContent = text;
|
||||
@@ -2046,6 +2063,7 @@ function renderHeader(): void {
|
||||
$('project-line').textContent = parts.join(' · ');
|
||||
$('project-line').title = parts.join(' · ');
|
||||
setHeaderStatus('step1-status', '', pier?.model ? '생성됨' : '미생성');
|
||||
setHeaderStatus('step-drawing-status', '', pier?.model ? '가능' : '형상 필요');
|
||||
renderSaveStatus();
|
||||
const total = pendingTotal();
|
||||
const targets = generationTargets();
|
||||
@@ -2214,6 +2232,7 @@ function renderAll(): void {
|
||||
renderReviewTasks();
|
||||
renderRebarColor();
|
||||
if (state.stage === 'quantity') renderQuantityScreen();
|
||||
if (state.stage === 'drawing') drawingWorkspace.update();
|
||||
scheduleBackup();
|
||||
}
|
||||
|
||||
@@ -2686,20 +2705,22 @@ function setStage(stage: Stage): void {
|
||||
// Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout.
|
||||
// Entering or leaving ② or ③ changes which objects are drawn (visibility.ts), so the scene is rebuilt then.
|
||||
const selectionOnly = (s: Stage) => s === 'plan' || s === 'rebar';
|
||||
if ((allPiers !== null) !== multiSceneOn() || selectionOnly(stage) !== selectionOnly(previous) || (stage === 'rebar') !== (previous === 'rebar')) setupScene();
|
||||
// ④ 도면작성 hides the 3D view, so the scene is not rebuilt for it (origin).
|
||||
if (stage !== 'drawing' && ((allPiers !== null) !== multiSceneOn() || selectionOnly(stage) !== selectionOnly(previous) || (stage === 'rebar') !== (previous === 'rebar'))) setupScene();
|
||||
if (state.scene) setRebarView(state.scene, stage === 'rebar');
|
||||
for (const g of abutGroups) setRebarViewOn(g, stage === 'rebar');
|
||||
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
|
||||
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
|
||||
$<HTMLElement>('view-rebar').hidden = stage !== 'rebar';
|
||||
$<HTMLElement>('view-quantity').hidden = stage !== 'quantity';
|
||||
$<HTMLElement>('view-drawing').hidden = stage !== 'drawing';
|
||||
if (stage === 'quantity') renderQuantityScreen();
|
||||
// Move the viewer after its new parent is visible, so that resize() measures the real size.
|
||||
placeViewer(stage);
|
||||
if (rebar3d) rebar3d.group.visible = stage === 'rebar';
|
||||
// The highlight is the 'current' class (style.css); aria-current is set alongside it for assistive tech.
|
||||
// The 재료표 screen belongs to ③, so ③ stays highlighted while it is open.
|
||||
const nav: [string, Stage[]][] = [['nav-shape', ['shape']], ['nav-plan', ['plan']], ['nav-rebar', ['rebar', 'quantity']]];
|
||||
const nav: [string, Stage[]][] = [['nav-shape', ['shape']], ['nav-plan', ['plan']], ['nav-rebar', ['rebar', 'quantity']], ['nav-drawing', ['drawing']]];
|
||||
for (const [id, keys] of nav) {
|
||||
const btn = $<HTMLButtonElement>(id);
|
||||
btn.classList.toggle('current', keys.includes(stage));
|
||||
@@ -4605,6 +4626,9 @@ function renderQuantityScreen(): void {
|
||||
const entries = targets.flatMap((pier) =>
|
||||
pier.model ? [{ pier, q: pierQuantity(pier) }] : [],
|
||||
);
|
||||
quantityRebarTon.clear();
|
||||
for(const {pier,q} of entries)quantityRebarTon.set(pier.input.name,q.missing.length?null:q.kg/1000);
|
||||
quantityWorkspace.refresh();
|
||||
// 전체 with more than one pier: the bridge total first (교량 전체 집계); the per-pier tables follow.
|
||||
if (state.quantityAll && entries.length > 1) body.appendChild(bridgeTotalBlock(aggregateQuantities(entries.map((e) => e.q)), entries.length));
|
||||
for (const { pier, q } of entries) {
|
||||
@@ -5105,7 +5129,12 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void {
|
||||
importInput.value = '';
|
||||
}
|
||||
});
|
||||
const importLabel = el('label', '지반 모델 파일 불러오기 (json)');
|
||||
const groundLaunch = el('button', '지반 모델 파일 불러오기 · 지반 생성 프로그램');
|
||||
groundLaunch.type = 'button';groundLaunch.className = 'btn';
|
||||
groundLaunch.addEventListener('click', () => window.dispatchEvent(new Event('pierzainer:open-ground')));
|
||||
wrap.appendChild(groundLaunch);
|
||||
const linkStatus = el('p', '현재 Excel을 사용하여 종평면도에서 지반을 생성합니다.', 'muted small');linkStatus.id='ground-link-status';wrap.appendChild(linkStatus);
|
||||
const importLabel = el('label', '기존 지반 JSON 직접 불러오기');
|
||||
importLabel.appendChild(importInput);
|
||||
wrap.appendChild(importLabel);
|
||||
|
||||
@@ -6016,16 +6045,8 @@ function applyImport(): void {
|
||||
setStatus(`${state.piers.length}기 형상을 생성했습니다.${abutNote} 정보·경고는 오른쪽 알림에서 확인하세요.`);
|
||||
}
|
||||
|
||||
function openImport(): void {
|
||||
const landing = document.querySelector<HTMLDivElement>('.landing-overlay');
|
||||
if (landing && !landing.hidden) return;
|
||||
const dlg = $<HTMLDialogElement>('import-dialog');
|
||||
if (!dlg.open) dlg.showModal();
|
||||
$<HTMLInputElement>('project-name').focus();
|
||||
}
|
||||
|
||||
async function fetchSample(): Promise<Uint8Array> {
|
||||
const res = await fetch(new URL('samples/운곡교P1입력.xlsx', document.baseURI));
|
||||
const res = await fetch(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
|
||||
if (!res.ok) throw new Error(`샘플 응답 ${res.status}`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
}
|
||||
@@ -6034,7 +6055,6 @@ declare global {
|
||||
interface Window {
|
||||
loadBytesToApp?: (bytes: Uint8Array, fileName: string) => void;
|
||||
openSampleProject?: () => Promise<void>;
|
||||
openDrawingFlow?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6056,16 +6076,6 @@ window.openSampleProject = async () => {
|
||||
setError(error instanceof Error ? error.message : '샘플 교량 모델을 불러오지 못했습니다.');
|
||||
}
|
||||
};
|
||||
window.openDrawingFlow = () => {
|
||||
const app = document.querySelector<HTMLElement>('.app');
|
||||
const landing = document.querySelector<HTMLDivElement>('.landing-overlay');
|
||||
if (app) {
|
||||
app.hidden = false;
|
||||
app.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
if (landing) landing.hidden = true;
|
||||
setStatus('도면 입력은 현재 개발 중입니다. 기존 도면 분석 모듈과 연결되기 전까지는 파일 선택 흐름만 유지합니다.');
|
||||
};
|
||||
|
||||
/** Piers that need a (re)build: never generated, or with changes not yet in the 3D model. */
|
||||
function generationTargets(): Pier[] {
|
||||
@@ -6087,7 +6097,6 @@ function regenerateChanged(): void {
|
||||
}
|
||||
|
||||
// --- events -----------------------------------------------------------------------------
|
||||
$<HTMLButtonElement>('open-import').addEventListener('click', openImport);
|
||||
$<HTMLButtonElement>('dlg-close').addEventListener('click', () => {
|
||||
$<HTMLDialogElement>('import-dialog').close();
|
||||
document.querySelector<HTMLElement>('.landing-overlay')?.classList.remove('modal-open');
|
||||
@@ -6102,6 +6111,7 @@ $<HTMLButtonElement>('nav-shape').addEventListener('click', () => setStage('shap
|
||||
$<HTMLButtonElement>('nav-plan').addEventListener('click', () => setStage('plan'));
|
||||
$<HTMLButtonElement>('nav-rebar').addEventListener('click', () => setStage('rebar'));
|
||||
$<HTMLButtonElement>('quantity-back').addEventListener('click', () => setStage('rebar'));
|
||||
$<HTMLButtonElement>('nav-drawing').addEventListener('click', () => setStage('drawing'));
|
||||
$<HTMLInputElement>('mat-fck').addEventListener('change', (ev) => setMaterial('fck', (ev.target as HTMLInputElement).value));
|
||||
$<HTMLInputElement>('mat-waste').addEventListener('change', (ev) => setWaste((ev.target as HTMLInputElement).value));
|
||||
for (const k of ['small16', 'main25', 'main35', 'main51'] as const) {
|
||||
|
||||
Reference in new issue
Block a user