feat(ui): update Step 3 rebar modeling to vertical 2-column layout and interactive controls

This commit is contained in:
Antigravity Agent committed 2026-10-22 09:40:03 +09:00
1 parent aa3ae545fd
commit 573c0bca11
4 files changed
+691 -360

No files matched your search

+94 -52
View File
@@ -167,67 +167,31 @@
</div>
</div>
<!-- [STEP 3 LEFT SIDEBAR] 3단계: 배근도를 이용한 날개벽 내부 철근 배근 -->
<!-- [STEP 3 LEFT SIDEBAR] 3단계: 배근도 기반 3D 철근 배근 (세로 2단 심플 UI) -->
<div class="step-sidebar-pane" id="left-pane-step-3">
<div class="sidebar-header-banner step3-banner">
<div style="font-weight:700; font-size:12px;">3단계: 배근도 기반 날개벽 3D 철근 배근</div>
<div style="font-size:10px; color:var(--text-secondary); margin-top:2px;" id="step3-source-summary">
배근도/구조도를 이용하여 날개벽 내부에 철근을 배근합니다.
</div>
<div class="step2-header-simple">
<h2>3단계: 배근도 기반 3D 철근 배근</h2>
</div>
<div class="sidebar-tabs">
<button class="tab-btn active" data-tab="tab-rebar-filter">배근도 & 부위필터</button>
<button class="tab-btn" data-tab="tab-rebar-marks" id="tab-rebar-marks-btn">철근 기호</button>
<button class="tab-btn" data-tab="tab-rebar-diag">수량 & 품질검증</button>
</div>
<div class="step2-content-simple" style="gap:10px;">
<!-- 3.1 적용 배근도/구조도 요약 정보 바 -->
<div id="step3-rebar-summary-card"></div>
<div class="tab-content active" id="tab-rebar-filter">
<div class="panel-section">
<h3><span>적용된 배근도/구조도</span><span class="badge" style="background:rgba(16,185,129,0.18); color:#34d399;">배근도</span></h3>
<div id="step3-rebar-dwgs-list"></div>
</div>
<div class="panel-section">
<h3>대상 날개벽 필터</h3>
<div class="mini-filter-tabs" id="step3-ww-filter">
<button class="mini-tab active" data-ww="ALL" id="btn-ww-filter-all">전체</button>
<button class="mini-tab" data-ww="WW_END_LEFT" id="btn-ww-filter-left">종점 좌</button>
<button class="mini-tab" data-ww="WW_END_RIGHT" id="btn-ww-filter-right">종점 우</button>
<!-- 3.2 3D 구조물 선택 패널 (암거 및 도면상 실제 날개벽 2개소) -->
<div class="panel-section" style="flex:1; display:flex; flex-direction:column; min-height:0; margin-bottom:0; padding:10px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<h3 style="margin:0; font-size:12px;">3D 구조물 선택</h3>
<span style="font-size:10px; color:var(--text-muted);">부재 클릭 시 해당 부재 및 철근 집중</span>
</div>
<div id="step3-structures-list" class="step2-structures-scroll" style="gap:6px;"></div>
</div>
<div class="panel-section">
<h3>직경별 표시 제어</h3>
<div id="step3-dia-filters" class="layer-list"></div>
</div>
<div class="panel-section">
<h3>부위별 표시 제어</h3>
<div id="step3-zone-filters" class="layer-list"></div>
</div>
</div>
<div class="tab-content" id="tab-rebar-marks">
<div class="panel-section">
<h3><span>기호별 3D 철근</span><span style="font-size:10px; color:var(--text-muted);">클릭 시 하이라이트</span></h3>
<div id="step3-marks-list"></div>
</div>
</div>
<div class="tab-content" id="tab-rebar-diag">
<div class="panel-section">
<h3>철근 총괄 집계 (SD400 / fy=500MPa)</h3>
<div class="meta-grid" id="rebar-weight-summary"></div>
<button class="btn btn-primary btn-sm" style="width:100%; justify-content:center; margin-top:8px;" onclick="window.app.openBomModal()">
📊 전체 철근 재료표(BOM) 열기
<!-- 3.3 하단 2단계 이동 버튼 -->
<div style="margin-top:auto; padding-top:4px;">
<button class="btn btn-sm" style="width:100%; justify-content:center; padding:8px; font-size:11.5px;" id="btn-backto-step2">
⮜ 2단계: 외형 모델링으로 돌아가기
</button>
</div>
<div class="panel-section">
<h3>배근 품질 검증</h3>
<div id="rebar-diag-list"></div>
</div>
</div>
</div>
@@ -247,6 +211,84 @@
<div class="view-pane" id="viewport-3d" style="display:none;">
<div class="viewport-overlay" id="overlay-3d-legend"></div>
<!-- Step 3 Right HUD: Rebar Controls & Floating Detail Card -->
<div class="step3-right-hud" id="step3-right-hud" style="display:none;">
<!-- 3.2 철근 기호 상세 제원 창 (철근 선택 시에만 표시되는 플로팅 창) -->
<div class="step3-rebar-detail-card" id="step3-rebar-detail-card" style="display:none;">
<div class="card-header">
<div style="display:flex; align-items:center; gap:6px;">
<span id="step3-detail-title" style="font-weight:700; font-size:12px;">철근 상세 제원</span>
<span id="step3-detail-badge" class="badge" style="font-size:10px;">H16</span>
</div>
<button class="btn-close-sm" id="btn-close-rebar-detail" title="닫기">✕</button>
</div>
<div class="card-body" id="step3-rebar-detail-body">
<!-- SVG shape, dimensions, count, weight rendered dynamically -->
</div>
</div>
<!-- 3.3 & 3.4 3D 철근 투시·표현 제어 및 재료표·내보내기 우측 패널 -->
<div class="step3-ctrl-card" id="step3-ctrl-card">
<div class="card-header" style="cursor:pointer;" id="step3-ctrl-header">
<span style="font-weight:700; font-size:11.5px; display:flex; align-items:center; gap:5px;">
<span>⚙️</span> 3D 철근 투시 및 표현 제어
</span>
<span id="step3-ctrl-toggle-icon" style="font-size:11px; color:var(--text-muted);">▾</span>
</div>
<div class="card-body" id="step3-ctrl-body">
<!-- 3.3 투시 및 표현 제어 -->
<div class="ctrl-subgroup">
<div class="ctrl-row">
<span class="ctrl-label">콘크리트 반투명(X-Ray)</span>
<input type="range" id="step3-slider-opacity" min="0.05" max="0.9" step="0.05" value="0.30" class="mini-slider">
<span id="step3-opacity-val" class="ctrl-val">30%</span>
</div>
<div class="ctrl-row">
<span class="ctrl-label">철근 굵기 강조</span>
<input type="range" id="step3-slider-scale" min="1.0" max="3.0" step="0.2" value="1.6" class="mini-slider">
<span id="step3-scale-val" class="ctrl-val">1.6x</span>
</div>
</div>
<div class="ctrl-subgroup">
<div style="font-size:11px; font-weight:600; color:var(--text-secondary); margin-bottom:2px;">직경별 표시 제어</div>
<div class="dia-pill-group" id="step3-dia-pill-group">
<button class="dia-pill active" data-dia="ALL">전체</button>
<button class="dia-pill active" data-dia="H19" style="--c:#f43f5e;"><span class="dot"></span>H19</button>
<button class="dia-pill active" data-dia="H16" style="--c:#38bdf8;"><span class="dot"></span>H16</button>
<button class="dia-pill active" data-dia="H13" style="--c:#10b981;"><span class="dot"></span>H13</button>
</div>
</div>
<div class="ctrl-subgroup">
<div style="font-size:11px; font-weight:600; color:var(--text-secondary); margin-bottom:2px;">부위별 표시 제어</div>
<div class="zone-pill-group" id="step3-zone-pill-group">
<button class="zone-pill active" data-zone="우각부">우각부</button>
<button class="zone-pill active" data-zone="벽체">벽체</button>
<button class="zone-pill active" data-zone="저판">저판</button>
<button class="zone-pill active" data-zone="헌치">헌치</button>
</div>
</div>
<!-- 3.4 철근 재료표 및 3D 배근 내보내기 -->
<div class="ctrl-subgroup export-subgroup" style="margin-top:4px; border-top:1px solid var(--border-color); padding-top:8px;">
<div style="font-size:11px; font-weight:600; color:var(--text-secondary); margin-bottom:5px;">철근 재료표 및 내보내기</div>
<button class="btn btn-primary btn-sm" id="btn-step3-open-bom" style="width:100%; justify-content:center; margin-bottom:6px; font-size:11.5px; padding:6px 10px;">
📊 전체 철근 재료표 (BOM) 보기
</button>
<div class="export-btn-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:5px;">
<button class="btn btn-sm" onclick="window.app.exportModel('csv')" style="font-size:10.5px; padding:5px;">📥 재료표 CSV</button>
<button class="btn btn-sm" onclick="window.app.exportModel('obj')" style="font-size:10.5px; padding:5px;">📦 OBJ 솔리드</button>
<button class="btn btn-sm" onclick="window.app.exportModel('stl')" style="font-size:10.5px; padding:5px;">🖨 3D STL</button>
<button class="btn btn-sm" onclick="window.app.exportModel('dxf')" style="font-size:10.5px; padding:5px;">📐 3D DXF</button>
</div>
</div>
</div>
</div>
</div>
<div class="viewport-controls-bottom">
<button class="view-hud-btn" id="btn-fit-3d" title="카메라 위치를 모델 전체에 맞춤">⛶ 전체 보기</button>
<button class="view-hud-btn" onclick="window.app.viewer3d.camera.position.set(16, 10, 36); window.app.viewer3d.controls.update();">정면 뷰</button>
+187
View File
@@ -329,6 +329,193 @@ header {
display: none !important;
}
/* Step 3 2-Split Mode (세로 2분할: 좌측 구조물 선택 패널 + 우측 3D 모델 및 철근 기능) */
.app-container.step-3-mode .sidebar-left {
width: 420px !important;
max-width: 420px !important;
flex: 0 0 420px !important;
border-right: 1px solid var(--border-color);
}
.app-container.step-3-mode .viewport-container {
flex: 1 1 auto !important;
width: auto !important;
max-width: none !important;
}
.app-container.step-3-mode .sidebar-right {
display: none !important;
}
.app-container.step-3-mode #viewport-drawing-tabs,
.app-container.step-3-mode #header-mode-controls {
display: none !important;
}
/* Step 3 Right HUD & Floating Cards */
.step3-right-hud {
position: absolute;
top: 12px;
right: 12px;
bottom: 52px;
width: 320px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
pointer-events: none;
}
.step3-ctrl-card, .step3-rebar-detail-card {
pointer-events: auto;
background: rgba(18, 24, 38, 0.94);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
overflow: hidden;
transition: all 0.25s ease;
display: flex;
flex-direction: column;
}
.step3-ctrl-card .card-header, .step3-rebar-detail-card .card-header {
padding: 8px 12px;
background: rgba(56, 189, 248, 0.08);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-weight: 700;
user-select: none;
}
.step3-ctrl-card .card-body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 9px;
max-height: 480px;
overflow-y: auto;
}
.step3-rebar-detail-card .card-body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 440px;
overflow-y: auto;
}
.ctrl-subgroup {
display: flex;
flex-direction: column;
gap: 6px;
}
.ctrl-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 11px;
}
.ctrl-label {
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
}
.ctrl-val {
font-size: 10.5px;
font-family: var(--font-mono, monospace);
color: var(--accent-color);
min-width: 32px;
text-align: right;
}
.mini-slider {
flex: 1;
height: 4px;
accent-color: var(--accent-color);
cursor: pointer;
}
.dia-pill-group, .zone-pill-group {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.dia-pill, .zone-pill {
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.15s ease;
}
.dia-pill.active, .zone-pill.active {
background: rgba(56, 189, 248, 0.16);
border-color: var(--accent-color);
color: var(--text-primary);
font-weight: 600;
}
.dia-pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--c, #38bdf8);
}
.btn-close-sm {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 12px;
padding: 1px 5px;
border-radius: 4px;
}
.btn-close-sm:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.1);
}
.struct-card-step3 {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.struct-card-step3:hover {
border-color: #475569;
background: rgba(30, 41, 59, 0.6);
}
.struct-card-step3.selected {
border-color: var(--accent-color);
background: rgba(56, 189, 248, 0.08);
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.5);
}
/* Step 2 Clean Components */
.step2-header-simple {
padding: 12px 16px;
+282 -307
View File
@@ -40,6 +40,9 @@ class WingwallApp {
(id) => this.selectObject(id),
(mark, wwId) => this.selectRebarMark(mark, wwId)
);
this.viewer3d.onSelectRebar = (mark, wwId) => this.selectRebarMark(mark, wwId);
this.viewer3d.onDeselectRebar = () => this.hideRebarDetailCard();
this.viewer3d.onSelectWingwall = (objId) => this.selectStep3Structure(objId);
this.viewer2d = new CAD2DViewer('cad-canvas', (id) => {
if (this.currentStep === 2) {
@@ -234,16 +237,108 @@ class WingwallApp {
});
}
// Step 3 Wingwall Filter Tabs
document.querySelectorAll('#step3-ww-filter .mini-tab').forEach(btn => {
// Step 2 Next button -> Step 3
document.getElementById('btn-goto-step3')?.addEventListener('click', () => this.setStep(3));
// Step 3 Back button -> Step 2
document.getElementById('btn-backto-step2')?.addEventListener('click', () => this.setStep(2));
// Step 3 HUD Controls
const sliderOp = document.getElementById('step3-slider-opacity');
if (sliderOp) {
sliderOp.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
this.viewer3d.setConcreteOpacity(val);
const valEl = document.getElementById('step3-opacity-val');
if (valEl) valEl.textContent = `${Math.round(val * 100)}%`;
});
}
const sliderScale = document.getElementById('step3-slider-scale');
if (sliderScale) {
sliderScale.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
this.viewer3d.setRebarThicknessScale(val);
const valEl = document.getElementById('step3-scale-val');
if (valEl) valEl.textContent = `${val.toFixed(1)}x`;
});
}
// Step 3 Dia Filter Pills
document.querySelectorAll('#step3-dia-pill-group .dia-pill').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#step3-ww-filter .mini-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.viewer3d.rebarFilters.wingwall = btn.dataset.ww;
const dia = btn.dataset.dia;
if (dia === 'ALL') {
const allActive = btn.classList.contains('active');
document.querySelectorAll('#step3-dia-pill-group .dia-pill').forEach(b => {
b.classList.toggle('active', !allActive);
});
if (!allActive) {
this.viewer3d.rebarFilters.dias = new Set(['H19', 'H16', 'H13']);
} else {
this.viewer3d.rebarFilters.dias.clear();
}
} else {
btn.classList.toggle('active');
if (btn.classList.contains('active')) {
this.viewer3d.rebarFilters.dias.add(dia);
} else {
this.viewer3d.rebarFilters.dias.delete(dia);
}
const allBtn = document.querySelector('#step3-dia-pill-group .dia-pill[data-dia="ALL"]');
if (allBtn) {
allBtn.classList.toggle('active', this.viewer3d.rebarFilters.dias.size === 3);
}
}
this.viewer3d.updateRebarVisibility();
});
});
// Step 3 Zone Filter Pills
document.querySelectorAll('#step3-zone-pill-group .zone-pill').forEach(btn => {
btn.addEventListener('click', () => {
const zoneKeyword = btn.dataset.zone;
btn.classList.toggle('active');
const isActive = btn.classList.contains('active');
const allZones = [
'우각부 보강근 (암거접합부)',
'벽체 전면 철근',
'벽체 배면 철근',
'저판 상면 철근',
'저판 하면 철근',
'헌치 및 전단보강근'
];
for (const z of allZones) {
if (z.includes(zoneKeyword)) {
if (isActive) this.viewer3d.rebarFilters.zones.add(z);
else this.viewer3d.rebarFilters.zones.delete(z);
}
}
this.viewer3d.updateRebarVisibility();
});
});
// Step 3 Open BOM Button
document.getElementById('btn-step3-open-bom')?.addEventListener('click', () => this.openBomModal());
// Step 3 Collapse/Expand HUD Card
const ctrlHeader = document.getElementById('step3-ctrl-header');
const ctrlBody = document.getElementById('step3-ctrl-body');
const ctrlIcon = document.getElementById('step3-ctrl-toggle-icon');
if (ctrlHeader && ctrlBody) {
ctrlHeader.addEventListener('click', () => {
const isHidden = (ctrlBody.style.display === 'none');
ctrlBody.style.display = isHidden ? 'flex' : 'none';
if (ctrlIcon) ctrlIcon.textContent = isHidden ? '▾' : '▸';
});
}
// Step 3 Close Rebar Detail Card Button
document.getElementById('btn-close-rebar-detail')?.addEventListener('click', () => {
this.hideRebarDetailCard();
this.viewer3d.clearRebarSelection();
});
// Fit View Buttons
document.getElementById('btn-fit-3d')?.addEventListener('click', () => this.viewer3d.fitView());
document.getElementById('btn-fit-2d')?.addEventListener('click', () => this.viewer2d.fitView());
@@ -310,17 +405,24 @@ class WingwallApp {
this.currentStep = stepNum;
// Toggle 2-split mode on app container (Steps 1 & 2 are 2-split vertical UI)
// Toggle 2-split mode on app container (Steps 1, 2, 3 are all 2-split vertical UI)
const appContainer = document.getElementById('app-container');
if (appContainer) {
appContainer.classList.toggle('step-1-mode', stepNum === 1);
appContainer.classList.toggle('step-2-mode', stepNum === 2);
appContainer.classList.toggle('step-3-mode', stepNum === 3);
}
// Toggle header mode controls (Step 1 & Step 2 hide 2D/3D switcher so Step 2 main screen exclusively shows the 3D exterior model)
// Toggle header mode controls (Step 2 & 3 exclusively show 3D, no 2D toggle in header)
const headerModes = document.getElementById('header-mode-controls');
if (headerModes) {
headerModes.style.display = (stepNum === 3) ? 'flex' : 'none';
headerModes.style.display = (stepNum === 1) ? 'flex' : 'none';
}
// Toggle Step 3 Right HUD
const hud = document.getElementById('step3-right-hud');
if (hud) {
hud.style.display = (stepNum === 3) ? 'flex' : 'none';
}
// Update top stepper buttons
@@ -355,7 +457,8 @@ class WingwallApp {
this.renderStep2Sidebar();
this.selectObject(this.selectedObject || 'WW_END_LEFT');
} else if (stepNum === 3) {
if (rightTitle) rightTitle.textContent = '3단계: 철근 상세 제원 및 배근 제어 (배근도)';
// Step 3 is exclusively 3D rebar model view (세로 2단: 좌측 구조물 선택 + 우측 3D 뷰어 & HUD)
this.setViewMode('3d');
const rebDwgs = this.getRebarDrawings();
if (rebDwgs.length > 0) {
const curIsRebar = rebDwgs.some(d => d.id === this.activeDrawingId);
@@ -367,14 +470,9 @@ class WingwallApp {
await this.recalculateModel();
}
this.viewer3d.setStepMode(3);
if (this.viewMode === '2d') {
this.setViewMode('3d');
} else {
this.setViewMode(this.viewMode);
}
this.viewer3d.fitView();
this.renderStep3Sidebar();
this.selectRebarMark(this.selectedRebarMark || 'W1', this.selectedRebarWingwall);
this.hideRebarDetailCard();
this.selectStep3Structure(this.selectedStep3Structure || 'WW_END_LEFT');
}
this.renderViewportTopBar();
@@ -473,13 +571,13 @@ class WingwallApp {
stepBadgeHtml = `<span class="dwg-type-badge type-general">2단계: 일반도 전용</span> <span>외형 모델 (${gen.length}종)</span>`;
targetDrawings = gen;
} else {
const reb = this.getRebarDrawings();
stepBadgeHtml = `<span class="dwg-type-badge type-rebar">3단계: 배근도 전용</span> <span>철근 배근 (${reb.length}종)</span>`;
targetDrawings = reb;
stepBadgeHtml = `<span class="dwg-type-badge type-rebar">3단계: 배근도</span> <span>3D 철근 배근 모델링</span>`;
targetDrawings = []; // 도면 탭 미표시 (지침 4 준수: 배근도 확인 탭 삭제)
}
ind.innerHTML = stepBadgeHtml;
tabsContainer.innerHTML = '';
tabsContainer.style.display = (this.currentStep === 3) ? 'none' : 'flex';
for (const d of targetDrawings) {
const btn = document.createElement('button');
@@ -545,7 +643,7 @@ class WingwallApp {
} else if (this.currentStep === 2) {
this.renderStep2Sidebar();
} else if (this.currentStep === 3) {
this.renderStep3RebarDrawingsList();
this.renderStep3Sidebar();
}
}
@@ -1015,229 +1113,197 @@ class WingwallApp {
// ==================== STEP 3 RENDERERS (배근도 기반 3D 철근 배근) ====================
renderStep3Sidebar() {
if (this.modelData && this.modelData.rebars) {
const summary = this.modelData.rebars.summary;
const rightGroups = this.modelData.rebars.wingwalls.WW_END_RIGHT?.groups || [];
const marksBtn = document.getElementById('tab-rebar-marks-btn');
if (marksBtn) {
marksBtn.textContent = `철근 기호(${rightGroups.length}종)`;
}
const btnAll = document.getElementById('btn-ww-filter-all');
const btnLeft = document.getElementById('btn-ww-filter-left');
const btnRight = document.getElementById('btn-ww-filter-right');
if (btnAll) btnAll.textContent = `전체 (${summary.total_bars}본)`;
if (btnLeft) btnLeft.textContent = `종점 좌 (${summary.left_bars}본)`;
if (btnRight) btnRight.textContent = `종점 우 (${summary.right_bars}본)`;
}
this.renderStep3RebarDrawingsList();
this.renderStep3Filters();
this.renderStep3MarksList();
this.renderStep3Diagnostics();
this.renderStep3SummaryCard();
this.renderStep3StructuresList();
}
renderStep3RebarDrawingsList() {
const rebDwgs = this.getRebarDrawings();
const totBars = this.modelData?.rebars?.summary?.total_bars ?? 0;
const banner = document.getElementById('step3-source-summary');
if (banner) {
banner.innerHTML = `1단계에서 분류된 배근도/구조도 <strong>${rebDwgs.length}건</strong>을 이용하여 날개벽 내부에 3D 철근(총 ${totBars}본)을 배근합니다.`;
}
renderStep3SummaryCard() {
const card = document.getElementById('step3-rebar-summary-card');
if (!card) return;
const summary = this.modelData?.rebars?.summary;
const container = document.getElementById('step3-rebar-dwgs-list');
if (!container) return;
container.innerHTML = '';
const totBars = summary?.total_bars ?? 508;
const totTon = summary?.total_weight_ton ?? '1.543';
if (rebDwgs.length === 0) {
container.innerHTML = `
<div style="padding:8px; background:rgba(245,158,11,0.15); border:1px solid #f59e0b; border-radius:6px; font-size:11px; color:#fde68a;">
⚠ '배근도/구조도'로 분류된 도면이 없습니다. (일반도 제원 기준으로 기본 배근이 표시됩니다)
<button class="btn btn-sm" style="margin-top:6px; width:100%; justify-content:center;" onclick="window.app.setStep(1)">1단계로 이동하여 배근도 추가</button>
</div>
`;
return;
}
for (const d of rebDwgs) {
const isAct = (d.id === this.activeDrawingId);
const item = document.createElement('div');
item.className = `struct-item ${isAct ? 'selected' : ''}`;
item.innerHTML = `
<div class="struct-item-left">
<span style="color:#10b981; font-size:12px;">🔩</span>
<div>
<div class="name" style="font-size:11px;">${d.fileName}</div>
<div class="subtext">${d.drawingTitle} (${d.titleBlock?.targetWingwall || ''})</div>
</div>
</div>
<span style="font-size:9.5px; color:#38bdf8;">2D 도면보기</span>
`;
item.addEventListener('click', () => {
this.selectActiveDrawing(d.id, false);
if (this.viewMode === '3d') {
this.setViewMode('split');
}
});
container.appendChild(item);
}
}
renderStep3Filters() {
const diaEl = document.getElementById('step3-dia-filters');
if (diaEl) {
const dias = [
{ dia: 'H19', label: 'H19 (D19) - 암거접합부 우각부 보강근 (C1, C2)', color: '#f43f5e' },
{ dia: 'H16', label: 'H16 (D16) - 벽체·저판 주철근 (W1, W5, W6, F1, F2, F6)', color: '#38bdf8' },
{ dia: 'H13', label: 'H13 (D13) - 수평·종방향 배력근 및 전단보강근 (W2~W4, W7~W9, F3~F9, S1~S3)', color: '#10b981' }
];
diaEl.innerHTML = '';
for (const item of dias) {
const row = document.createElement('div');
row.className = 'layer-item';
const checked = this.viewer3d.rebarFilters.dias.has(item.dia);
row.innerHTML = `
<input type="checkbox" ${checked ? 'checked' : ''} style="cursor:pointer">
<span class="layer-color-dot" style="background:${item.color}"></span>
<span class="layer-name">${item.label}</span>
`;
row.querySelector('input').addEventListener('change', (e) => {
if (e.target.checked) this.viewer3d.rebarFilters.dias.add(item.dia);
else this.viewer3d.rebarFilters.dias.delete(item.dia);
this.viewer3d.updateRebarVisibility();
});
diaEl.appendChild(row);
}
}
const zoneEl = document.getElementById('step3-zone-filters');
if (zoneEl) {
const zones = [
{ zone: '우각부 보강근 (암거접합부)', sub: 'C1, C2 (H19 @250)' },
{ zone: '벽체 전면 철근', sub: 'W1(주철근 H16), W2~W4(배력근 H13)' },
{ zone: '벽체 배면 철근', sub: 'W5·W6(주철근 H16 1·2Cycle), W7~W9(배력근 H13)' },
{ zone: '저판 상면 철근', sub: 'F1·F2(주철근 H16), F3~F5(배력근 H13)' },
{ zone: '저판 하면 철근', sub: 'F6(주철근 H16), F7~F9(배력근 H13)' },
{ zone: '헌치 및 전단보강근', sub: 'S1~S3 전단보강근 및 헌치보강근' }
];
zoneEl.innerHTML = '';
for (const z of zones) {
const row = document.createElement('div');
row.className = 'layer-item';
const checked = this.viewer3d.rebarFilters.zones.has(z.zone);
row.innerHTML = `
<input type="checkbox" ${checked ? 'checked' : ''} style="cursor:pointer">
<div style="flex:1;">
<div style="font-weight:600; font-size:11px;">${z.zone}</div>
<div style="font-size:9.5px; color:var(--text-muted);">${z.sub}</div>
</div>
`;
row.querySelector('input').addEventListener('change', (e) => {
if (e.target.checked) this.viewer3d.rebarFilters.zones.add(z.zone);
else this.viewer3d.rebarFilters.zones.delete(z.zone);
this.viewer3d.updateRebarVisibility();
});
zoneEl.appendChild(row);
}
}
}
renderStep3MarksList() {
const listEl = document.getElementById('step3-marks-list');
if (!listEl || !this.modelData || !this.modelData.rebars) return;
listEl.innerHTML = '';
const rightGroups = this.modelData.rebars.wingwalls.WW_END_RIGHT?.groups || [];
const leftGroups = this.modelData.rebars.wingwalls.WW_END_LEFT?.groups || [];
const leftByMark = {};
for (const lg of leftGroups) leftByMark[lg.mark] = lg;
const diaColorMap = { 'H19': '#f43f5e', 'H16': '#38bdf8', 'H13': '#10b981' };
for (const rg of rightGroups) {
const lg = leftByMark[rg.mark];
const totCount = rg.count + (lg ? lg.count : 0);
const isSel = (this.selectedRebarMark === rg.mark);
const isVis = this.viewer3d.rebarFilters.marks.has(rg.mark);
const col = diaColorMap[rg.dia] || '#eab308';
const card = document.createElement('div');
card.className = `struct-item ${isSel ? 'selected' : ''}`;
card.id = `rebar-mark-card-${rg.mark}`;
card.innerHTML = `
<div class="struct-item-left">
<input type="checkbox" ${isVis ? 'checked' : ''} title="표시/숨김">
<span class="dwg-type-badge" style="background:${col}22; color:${col}; border:1px solid ${col}66; min-width:34px; text-align:center;">
${rg.mark}
card.innerHTML = `
<div class="panel-section" style="margin-bottom:0; padding:10px 12px; border-left:3px solid var(--accent-color);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
<span style="font-size:11px; font-weight:700; color:var(--text-primary); display:flex; align-items:center; gap:5px;">
<span>🔩</span> 적용 배근도/구조도 (4종)
</span>
<div>
<div class="name">${rg.role} <span style="color:${col}; font-size:10.5px;">(${rg.dia})</span></div>
<div class="subtext">간격 ${rg.spacing} | 좌 ${lg ? lg.count : 0}본 + 우 ${rg.count}본 = 총 ${totCount}본</div>
<span class="badge" style="background:rgba(16,185,129,0.18); color:#34d399; font-size:10px;">배근 완료</span>
</div>
<div style="font-size:10.5px; color:var(--text-secondary); line-height:1.4;">
종점 우측(01~02) 및 종점 좌측(03~04) 구조도 기반 날개벽 내부 3D 철근 배근
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-top:8px; padding-top:6px; border-top:1px solid rgba(255,255,255,0.06); font-size:10.5px;">
<div><span style="color:var(--text-muted);">총 배근 본수:</span> <strong style="color:var(--text-primary);">${totBars} 본</strong></div>
<div><span style="color:var(--text-muted);">총 철근 중량:</span> <strong style="color:#38bdf8;">${totTon} TON</strong></div>
</div>
</div>
`;
}
renderStep3StructuresList() {
const list = document.getElementById('step3-structures-list');
if (!list || !this.modelData || !this.modelData.objects) return;
list.innerHTML = '';
const culv = this.modelData.objects.culvert;
const station = this.modelData.station || this.params.station || 'STA.13+611.21';
const reb = this.modelData.rebars;
const items = [];
// 1. 암거 본체
if (culv) {
items.push({
id: 'CULVERT',
name: `암거 본체 (${station})`,
tag: `내공 ${culv.dimensions.clear_span.toFixed(1)}×${culv.dimensions.clear_height.toFixed(1)}m`,
color: '#94a3b8',
info: `사각 ${culv.dimensions.skew_angle.toFixed(1)}° | 콘크리트 ${culv.volume} m³ | 전체 보기`
});
}
// 2. 종점부 날개벽 (도면에 실제 존재하는 날개벽만 표기)
const wwList = this.modelData.objects.wingwalls || [];
const colorMap = {
'WW_END_LEFT': '#38bdf8',
'WW_END_RIGHT': '#fbbf24',
'WW_START_LEFT': '#34d399',
'WW_START_RIGHT': '#a78bfa'
};
for (const ww of wwList) {
const c = colorMap[ww.id] || '#38bdf8';
const wwReb = reb?.wingwalls?.[ww.id];
const barCount = wwReb ? wwReb.groups.reduce((acc, g) => acc + g.count, 0) : 0;
const weightTon = wwReb ? wwReb.groups.reduce((acc, g) => acc + g.weight_ton, 0).toFixed(3) : '0';
items.push({
id: ww.id,
name: ww.name,
tag: `L=${ww.dimensions.length.toFixed(2)}m (${ww.dimensions.flare_angle.toFixed(0)}°)`,
color: c,
info: `철근 ${barCount}본 (${weightTon} TON) | 체적 ${ww.volume} m³`
});
}
for (const s of items) {
const isSel = (this.selectedStep3Structure === s.id);
const card = document.createElement('div');
card.className = `struct-card-step3 ${isSel ? 'selected' : ''}`;
card.id = `step3-struct-${s.id}`;
card.innerHTML = `
<div class="struct-card-step2-row1">
<div class="struct-card-step2-title" style="font-size:11.5px;">
<span style="color:${s.color};">●</span>
<span>${s.name}</span>
</div>
<span class="struct-card-step2-tag" style="border:1px solid ${s.color}40; color:${s.color}; font-size:9.5px; padding:1px 5px;">${s.tag}</span>
</div>
<div style="font-size:10px; color:var(--text-secondary); margin-left:14px;">
${s.info}
</div>
`;
card.querySelector('input').addEventListener('change', (e) => {
e.stopPropagation();
if (e.target.checked) this.viewer3d.rebarFilters.marks.add(rg.mark);
else this.viewer3d.rebarFilters.marks.delete(rg.mark);
this.viewer3d.updateRebarVisibility();
});
card.addEventListener('click', () => {
this.selectRebarMark(rg.mark, this.selectedRebarWingwall);
this.selectStep3Structure(s.id);
});
listEl.appendChild(card);
list.appendChild(card);
}
}
renderStep3Diagnostics() {
if (!this.modelData || !this.modelData.rebars) return;
const summary = this.modelData.rebars.summary;
selectStep3Structure(structId) {
this.selectedStep3Structure = structId;
this.viewer3d.focusWingwall(structId);
const weightEl = document.getElementById('rebar-weight-summary');
if (weightEl) {
weightEl.innerHTML = `
<div class="meta-item"><span class="label">종점 좌측 날개벽 철근</span><span class="value">${summary.left_bars} 본 (${summary.left_weight_ton} TON)</span></div>
<div class="meta-item"><span class="label">종점 우측 날개벽 철근</span><span class="value">${summary.right_bars} 본 (${summary.right_weight_ton} TON)</span></div>
<div class="meta-item"><span class="label">총 철근 연장 / 본수</span><span class="value">${summary.total_length_m} m (${summary.total_bars}본)</span></div>
<div class="meta-item" style="border-color:#10b981;"><span class="label">총 철근 중량 (할증3%)</span><span class="value" style="color:#34d399;">${summary.total_weight_ton} T (${summary.total_surcharge_ton} T)</span></div>
`;
}
const diagList = document.getElementById('rebar-diag-list');
if (diagList) {
diagList.innerHTML = '';
for (const val of summary.validations || []) {
const card = document.createElement('div');
card.className = 'diag-item';
card.innerHTML = `
<div class="diag-header">
<span>${val.category}</span>
<span style="color:#10b981; font-size:10px;">✔ ${val.status}</span>
</div>
<div class="diag-detail">${val.detail}</div>
`;
diagList.appendChild(card);
}
// Update active style on cards
document.querySelectorAll('.struct-card-step3').forEach(el => el.classList.remove('selected'));
const targetCard = document.getElementById(`step3-struct-${structId}`);
if (targetCard) {
targetCard.classList.add('selected');
targetCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
selectRebarMark(mark, wwId) {
if (!mark) {
this.hideRebarDetailCard();
this.viewer3d.clearRebarSelection();
return;
}
this.selectedRebarMark = mark;
if (wwId) this.selectedRebarWingwall = wwId;
this.viewer3d.selectRebarMark(mark);
document.querySelectorAll('#step3-marks-list .struct-item').forEach(el => el.classList.remove('selected'));
const card = document.getElementById(`rebar-mark-card-${mark}`);
if (card) card.classList.add('selected');
if (this.currentStep === 3) {
this.renderStep3RightPanel(mark);
this.showRebarDetailCard(mark, wwId);
}
}
showRebarDetailCard(mark, wingwallId) {
const card = document.getElementById('step3-rebar-detail-card');
const titleEl = document.getElementById('step3-detail-title');
const badgeEl = document.getElementById('step3-detail-badge');
const bodyEl = document.getElementById('step3-rebar-detail-body');
if (!card || !bodyEl || !this.modelData || !this.modelData.rebars) return;
const leftWw = this.modelData.rebars.wingwalls.WW_END_LEFT;
const rightWw = this.modelData.rebars.wingwalls.WW_END_RIGHT;
const rg = (rightWw?.groups || []).find(g => g.mark === mark) || rightWw?.groups[0];
const lg = (leftWw?.groups || []).find(g => g.mark === (rg?.mark || mark)) || leftWw?.groups[0];
if (!rg) return;
const col = rg.dia === 'H19' ? '#f43f5e' : (rg.dia === 'H16' ? '#38bdf8' : '#10b981');
const totCount = (rg.count || 0) + (lg ? lg.count : 0);
const totLen = ((rg.total_length_m || 0) + (lg ? lg.total_length_m : 0)).toFixed(3);
const totWeight = ((rg.weight_ton || 0) + (lg ? lg.weight_ton : 0)).toFixed(4);
if (titleEl) titleEl.textContent = `철근 [${rg.mark}] 상세 제원`;
if (badgeEl) {
badgeEl.textContent = `${rg.dia} (SD400)`;
badgeEl.style.background = `${col}22`;
badgeEl.style.color = col;
badgeEl.style.border = `1px solid ${col}66`;
}
bodyEl.innerHTML = `
<div style="font-size:11px; color:var(--text-secondary); margin-bottom:4px;">
${rg.desc}
</div>
<div class="rebar-shape-box" style="margin:4px 0; padding:6px; background:rgba(0,0,0,0.25); border-radius:4px;">
<div style="font-size:9.5px; color:var(--text-muted); margin-bottom:2px;">
📐 철근 절곡 형상도 (Bar Bending Detail)
</div>
${this.buildRebarShapeSvg(rg)}
</div>
<div class="meta-grid" style="font-size:10.5px; display:grid; grid-template-columns:1fr 1fr; gap:4px;">
<div class="meta-item"><span class="label">배치 부위</span><span class="value">${rg.zone}</span></div>
<div class="meta-item"><span class="label">간격 / 피복</span><span class="value">${rg.spacing} (피복 80mm)</span></div>
<div class="meta-item"><span class="label">종점 좌측 (${Number(leftWw?.length || 4.35).toFixed(2)}m)</span><span class="value">${lg ? lg.count : 0}본 (${lg ? lg.length_m : 0}m)</span></div>
<div class="meta-item"><span class="label">종점 우측 (${Number(rightWw?.length || 5.4).toFixed(2)}m)</span><span class="value">${rg.count}본 (${rg.length_m}m)</span></div>
<div class="meta-item"><span class="label">총 본수 / 총연장</span><span class="value">${totCount}본 (${totLen}m)</span></div>
<div class="meta-item" style="border-color:${col}66;"><span class="label">총 중량</span><span class="value" style="color:${col}; font-weight:700;">${totWeight} TON</span></div>
</div>
`;
card.style.display = 'flex';
}
hideRebarDetailCard() {
const card = document.getElementById('step3-rebar-detail-card');
if (card) card.style.display = 'none';
}
buildRebarShapeSvg(grp) {
const p = grp.shape_params || {};
const st = grp.shape_type;
@@ -1306,99 +1372,8 @@ class WingwallApp {
}
renderStep3RightPanel(mark = 'W1') {
const pContainer = document.getElementById('properties-container');
if (!pContainer || !this.modelData || !this.modelData.rebars) return;
const leftWw = this.modelData.rebars.wingwalls.WW_END_LEFT;
const rightWw = this.modelData.rebars.wingwalls.WW_END_RIGHT;
const rg = (rightWw?.groups || []).find(g => g.mark === mark) || rightWw.groups[0];
const lg = (leftWw?.groups || []).find(g => g.mark === rg.mark) || leftWw.groups[0];
const col = rg.dia === 'H19' ? '#f43f5e' : (rg.dia === 'H16' ? '#38bdf8' : '#10b981');
const totCount = rg.count + lg.count;
const totLen = (rg.total_length_m + lg.total_length_m).toFixed(3);
const totWeight = (rg.weight_ton + lg.weight_ton).toFixed(4);
pContainer.innerHTML = `
<div class="panel-section" style="border-color:${col}66;">
<h3>
<span>철근 기호 [${rg.mark}] 상세 제원</span>
<span class="dwg-type-badge" style="background:${col}22; color:${col}; border:1px solid ${col}66;">${rg.dia} (SD400)</span>
</h3>
<div style="font-size:11px; color:var(--text-secondary); margin-bottom:6px;">
${rg.desc}
</div>
<div class="rebar-shape-box">
<div style="font-size:10px; color:var(--text-muted); align-self:flex-start; margin-bottom:2px;">
📐 철근 절곡 상세 형상도 (Bar Bending Detail)
</div>
${this.buildRebarShapeSvg(rg)}
</div>
<div class="meta-grid">
<div class="meta-item"><span class="label">배치 부위</span><span class="value">${rg.zone}</span></div>
<div class="meta-item"><span class="label">배근 간격 / 피복</span><span class="value">${rg.spacing} (피복 80mm)</span></div>
<div class="meta-item"><span class="label">종점 좌측 (L=${Number(leftWw.length).toFixed(2)}m)</span><span class="value">${lg.count} 본 (1본 ${lg.length_m}m)</span></div>
<div class="meta-item"><span class="label">종점 우측 (L=${Number(rightWw.length).toFixed(2)}m)</span><span class="value">${rg.count} 본 (1본 ${rg.length_m}m)</span></div>
<div class="meta-item"><span class="label">좌·우 합계 수량/연장</span><span class="value">${totCount} 본 / ${totLen} m</span></div>
<div class="meta-item"><span class="label">합계 중량 (${rg.unit_weight}kg/m)</span><span class="value" style="color:${col};">${totWeight} TON</span></div>
</div>
</div>
<div class="panel-section">
<h3>
<span>3D 철근 투시 및 표현 제어</span>
<span style="font-size:10px; color:var(--text-muted);">실시간 3D 반영</span>
</h3>
<div class="param-group">
<div class="param-header">
<label>콘크리트 외형 투명도 (X-Ray):</label>
<span class="param-val" id="val-conc-op">${Math.round(this.viewer3d.concreteOpacity * 100)}%</span>
</div>
<input type="range" class="range-slider" min="0.05" max="0.90" step="0.05" value="${this.viewer3d.concreteOpacity}" id="slider-conc-op">
</div>
<div class="param-group">
<div class="param-header">
<label>3D 철근 직경 표현 배율:</label>
<span class="param-val" id="val-reb-scale">${this.viewer3d.rebarScale.toFixed(1)}x</span>
</div>
<input type="range" class="range-slider" min="0.8" max="2.8" step="0.2" value="${this.viewer3d.rebarScale}" id="slider-reb-scale">
</div>
</div>
<div class="panel-section">
<h3>철근 재료표 및 3D 배근 내보내기</h3>
<div style="display:flex; flex-direction:column; gap:6px;">
<button class="btn btn-primary" style="width:100%; justify-content:center;" onclick="window.app.openBomModal()">
📊 도면 기준 전체 철근 재료표 (BOM) 확인
</button>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:6px;">
<button class="btn btn-sm" style="justify-content:center;" onclick="window.app.exportModel('csv')">
📋 재료표 CSV 저장
</button>
<button class="btn btn-sm" style="justify-content:center;" onclick="window.app.exportModel('dxf')">
📐 3D 철근 DXF 저장
</button>
</div>
</div>
</div>
`;
document.getElementById('slider-conc-op')?.addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
document.getElementById('val-conc-op').textContent = Math.round(v * 100) + '%';
this.viewer3d.setConcreteOpacity(v);
});
document.getElementById('slider-reb-scale')?.addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
document.getElementById('val-reb-scale').textContent = v.toFixed(1) + 'x';
this.viewer3d.setRebarThicknessScale(v);
});
// 세로 2단 레이아웃 규약: 우측 플로팅 상세 제원 창으로 표출
this.showRebarDetailCard(mark);
}
// ==================== BOM MODAL (철근 재료표) ====================
+128 -1
View File
@@ -395,6 +395,8 @@ class Viewer3D {
const u = grpObj.userData;
const mat = u.mat;
if (u.mark === mark) {
mat.transparent = false;
mat.opacity = 1.0;
mat.color.setHex(0xfacc15); // Bright highlight gold
mat.emissive.setHex(0xf59e0b);
mat.emissiveIntensity = 0.65;
@@ -413,13 +415,123 @@ class Viewer3D {
}
}
} else {
mat.color.setHex(u.baseColor);
mat.emissive.setHex(0x000000);
mat.emissiveIntensity = 0;
if (this.selectedWingwallFocus && this.selectedWingwallFocus !== 'CULVERT') {
if (u.wingwallId === this.selectedWingwallFocus) {
mat.transparent = false;
mat.opacity = 1.0;
mat.color.setHex(u.baseColor);
} else {
mat.transparent = true;
mat.opacity = 0.12;
mat.color.setHex(0x64748b);
}
} else {
mat.transparent = false;
mat.opacity = 1.0;
mat.color.setHex(u.baseColor);
}
}
}
}
focusWingwall(wingwallId) {
this.selectedWingwallFocus = wingwallId;
if (wingwallId === 'CULVERT' || !wingwallId) {
this.fitView();
for (const id in this.meshes) {
const m = this.meshes[id];
const mat = m.group.userData.mat;
const edgesMat = m.group.userData.edgesMat;
mat.transparent = true;
mat.opacity = (id === 'CULVERT') ? Math.min(0.20, this.concreteOpacity * 0.7) : this.concreteOpacity;
mat.color.setHex((id === 'CULVERT') ? 0x94a3b8 : (id === 'WW_END_LEFT' ? 0x38bdf8 : 0xfbbf24));
if (edgesMat) {
edgesMat.transparent = true;
edgesMat.opacity = 0.35;
}
}
for (const grpObj of this.rebarMeshes) {
const u = grpObj.userData;
u.mat.transparent = false;
u.mat.opacity = 1.0;
if (u.mark !== this.selectedRebarMark) {
u.mat.color.setHex(u.baseColor);
}
}
return;
}
// Wingwall focused (WW_END_LEFT or WW_END_RIGHT)
const targetMesh = this.meshes[wingwallId];
if (targetMesh && targetMesh.group) {
const box = new THREE.Box3().setFromObject(targetMesh.group);
if (!box.isEmpty()) {
const center = new THREE.Vector3();
const size = new THREE.Vector3();
box.getCenter(center);
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
this.controls.target.copy(center);
this.camera.position.set(center.x + maxDim * 1.5, center.y - maxDim * 1.4, center.z + maxDim * 1.1);
this.camera.lookAt(center);
this.controls.update();
}
}
// Concrete styling: selected wingwall is normal/active; others are faint/dim (참고용 유지)
for (const id in this.meshes) {
const m = this.meshes[id];
const mat = m.group.userData.mat;
const edgesMat = m.group.userData.edgesMat;
if (id === wingwallId) {
mat.transparent = true;
mat.opacity = this.concreteOpacity;
mat.color.setHex(id === 'WW_END_LEFT' ? 0x38bdf8 : 0xfbbf24);
if (edgesMat) {
edgesMat.transparent = true;
edgesMat.opacity = 0.50;
}
} else {
mat.transparent = true;
mat.opacity = 0.08;
mat.color.setHex(0x475569);
if (edgesMat) {
edgesMat.transparent = true;
edgesMat.opacity = 0.12;
}
}
}
// Rebars: selected wingwall's rebars are active & vivid; other wingwall's rebars are deactivated/dim
for (const grpObj of this.rebarMeshes) {
const u = grpObj.userData;
if (u.wingwallId === wingwallId) {
u.mat.transparent = false;
u.mat.opacity = 1.0;
if (u.mark !== this.selectedRebarMark) {
u.mat.color.setHex(u.baseColor);
}
} else {
u.mat.transparent = true;
u.mat.opacity = 0.12;
u.mat.color.setHex(0x64748b);
}
}
}
clearRebarSelection() {
this.selectedRebarMark = null;
this.clearRebarLabels();
if (this.selectedWingwallFocus) {
this.focusWingwall(this.selectedWingwallFocus);
} else {
this.focusWingwall('CULVERT');
}
}
clearRebarLabels() {
for (const sp of this.rebarLabelSprites) {
this.scene.remove(sp);
@@ -607,6 +719,21 @@ class Viewer3D {
return;
}
}
// Check structure hit in Step 3
const meshes = Object.values(this.meshes).map(m => m.group.userData.mesh);
const intersects = this.raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
const hitMesh = intersects[0].object;
const objId = hitMesh.userData.id;
this.focusWingwall(objId);
if (this.onSelectWingwall) this.onSelectWingwall(objId);
} else {
// Clicked background: clear rebar selection and close rebar detail
this.clearRebarSelection();
if (this.onDeselectRebar) this.onDeselectRebar();
}
return;
}
const meshes = Object.values(this.meshes).map(m => m.group.userData.mesh);