Skip to content

Commit c5f8029

Browse files
committed
feat: add core application event and UI managers to handle pathfinding logic and visualization controls.
1 parent 3a149e6 commit c5f8029

3 files changed

Lines changed: 394 additions & 345 deletions

File tree

src/main.js

Lines changed: 8 additions & 345 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,9 @@ import InputForm from './components/InputForm.js';
99
import ProgressBar from './components/ProgressBar.js';
1010
import LayerControl from './components/LayerControl.js';
1111
import statusManager from './utils/statusManager.js';
12-
import exportManager from './utils/exportManager.js';
1312
import shareManager from './utils/shareManager.js';
14-
import layerToggleManager from './utils/layerToggleManager.js';
15-
// import navigatorManager from './utils/navigatorManager.js'; // 已禁用以优化性能
16-
import { findPathAStar } from './utils/pathfinding.js';
17-
import { smoothPathVisibility } from './utils/pathSmoothing.js';
13+
import UIManager from './managers/UIManager.js';
14+
import AppEventManager from './managers/AppEventManager.js';
1815

1916
// #TODO: 添加错误边界处理
2017
// #TODO: 添加性能监控
@@ -30,87 +27,10 @@ class App {
3027
this.isInitialized = false;
3128
// 性能计时(仅记录耗时,不显示 Loading)
3229
this.perf = { start: 0 };
33-
}
34-
35-
/**
36-
* 图例显隐控制(右侧面板)
37-
*/
38-
setupLegendControls() {
39-
const card = document.getElementById('legend-card');
40-
if (!card) return;
41-
42-
// 更新 UI 状态的辅助函数
43-
const updateUI = (key, visible) => {
44-
const item = card.querySelector(`.legend-item-new[data-layer="${key}"]`);
45-
if (item) {
46-
const btn = item.querySelector('.legend-eye');
47-
const isOn = visible !== false;
48-
item.classList.toggle('off', !isOn);
49-
if (btn) btn.setAttribute('aria-pressed', isOn ? 'true' : 'false');
50-
}
51-
};
52-
53-
// 初始化:根据管理器状态同步“眼睛”按钮
54-
try {
55-
const states = layerToggleManager.getLayerStates();
56-
Object.keys(states).forEach(key => {
57-
updateUI(key, states[key]);
58-
});
59-
} catch (e) {
60-
console.debug('[Legend] init sync skipped:', e);
61-
}
6230

63-
// 监听全局图层变化事件(实现双向同步)
64-
window.addEventListener('layer-visibility-changed', (e) => {
65-
if (e.detail) {
66-
const { layerName, isVisible } = e.detail;
67-
updateUI(layerName, isVisible);
68-
}
69-
});
70-
71-
// 事件委托:点击眼睛切换图层
72-
card.addEventListener('click', (ev) => {
73-
const btn = ev.target.closest('.legend-eye');
74-
if (!btn) return;
75-
const item = btn.closest('.legend-item-new');
76-
const key = item && item.getAttribute('data-layer');
77-
if (!key) return;
78-
79-
// 获取当前按钮状态并取反
80-
const currentPressed = btn.getAttribute('aria-pressed') === 'true';
81-
const next = !currentPressed;
82-
83-
// 仅调用管理器,UI 更新交由事件监听处理
84-
layerToggleManager.toggleLayer(key, next);
85-
});
86-
}
87-
88-
/**
89-
* 路径面板交互:清空/刷新/折叠
90-
*/
91-
setupPathPanelControls() {
92-
const clearBtn = document.getElementById('path-clear-btn');
93-
if (clearBtn) {
94-
clearBtn.addEventListener('click', () => {
95-
try { renderer.interaction && renderer.interaction.clearPath(); } catch (e) {}
96-
});
97-
}
98-
99-
const refreshBtn = document.getElementById('path-refresh-btn');
100-
if (refreshBtn) {
101-
refreshBtn.addEventListener('click', () => {
102-
try { renderer.interaction && renderer.interaction.redrawLastPath(); } catch (e) {}
103-
});
104-
}
105-
106-
const collapseBtn = document.getElementById('path-collapse-btn');
107-
const pathCard = document.getElementById('path-card');
108-
if (collapseBtn && pathCard) {
109-
collapseBtn.addEventListener('click', () => {
110-
const collapsed = pathCard.classList.toggle('collapsed');
111-
collapseBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
112-
});
113-
}
31+
// Managers
32+
this.uiManager = new UIManager(this);
33+
this.appEventManager = new AppEventManager(this);
11434
}
11535

11636
/**
@@ -150,176 +70,9 @@ class App {
15070
const tInit1 = performance?.now ? performance.now() : Date.now();
15171
this.perf.initRenderMs = Math.max(0, Math.round(tInit1 - tInit0));
15272

153-
// 初始化 FPS 开关状态同步到渲染器
154-
try {
155-
const fpsChk = document.getElementById('show-fps');
156-
if (fpsChk && typeof renderer.setFpsVisible === 'function') {
157-
renderer.setFpsVisible(!!fpsChk.checked);
158-
}
159-
} catch (_) {}
160-
161-
// 全屏切换功能
162-
const fullscreenBtn = document.getElementById('fullscreen-btn');
163-
const canvasContainer = document.querySelector('.canvas-container');
164-
fullscreenBtn.addEventListener('click', () => {
165-
console.log(
166-
`[UI] fullscreen button clicked. currentFS=${!!document.fullscreenElement}, pixiContainer=${
167-
pixiContainer.clientWidth
168-
}x${pixiContainer.clientHeight}`,
169-
);
170-
if (!document.fullscreenElement) {
171-
canvasContainer.requestFullscreen().catch((err) => {
172-
alert(`无法进入全屏模式: ${err.message}`);
173-
});
174-
} else {
175-
document.exitFullscreen();
176-
}
177-
});
178-
179-
// 监听全屏变化,调整渲染器尺寸
180-
document.addEventListener('fullscreenchange', () => {
181-
const isFullscreen = !!document.fullscreenElement;
182-
const canvasInfo = document.getElementById('canvas-info');
183-
184-
// 延迟一小段时间再调整尺寸,以确保 DOM 更新完毕
185-
setTimeout(() => {
186-
let newWidth, newHeight;
187-
if (isFullscreen) {
188-
newWidth = screen.width;
189-
const infoH = canvasInfo && canvasInfo.offsetHeight ? canvasInfo.offsetHeight : 0;
190-
newHeight = screen.height - infoH;
191-
} else {
192-
// 退出全屏时,强制使用容器的 clientWidth/Height
193-
newWidth = pixiContainer.clientWidth;
194-
newHeight = pixiContainer.clientHeight;
195-
}
196-
console.log(
197-
`[Resize][fullscreenchange] isFS=${isFullscreen} screen=${screen.width}x${screen.height} pixiContainer=${pixiContainer.clientWidth}x${pixiContainer.clientHeight} canvasInfoH=${canvasInfo?.offsetHeight} -> resize(${newWidth}x${newHeight})`,
198-
);
199-
renderer.resize(newWidth, newHeight);
200-
console.log(`[Resize][fullscreenchange] renderer.resize done.`);
201-
}, 100); // 100ms 延迟
202-
});
203-
204-
// 绑定 UI 事件
205-
this.setupUIEventHandlers();
206-
207-
// 绑定缩放工具栏按钮
208-
this.setupZoomControls();
209-
210-
// 绑定侧栏交互
211-
this.setupLegendControls();
212-
this.setupPathPanelControls();
213-
214-
// 监听 3D 渲染器的寻路请求
215-
window.addEventListener('renderer-path-request', (e) => {
216-
const { start, end } = e.detail;
217-
if (start && end && renderer.roadNetData) {
218-
console.log('Path request received:', start, end);
219-
const layerIndex = start.layer || 0;
220-
const layer = renderer.roadNetData.layers[layerIndex];
221-
222-
if (layer) {
223-
setTimeout(() => {
224-
const smoothStartTime = performance.now();
225-
226-
// 1. 使用A*算法查找原始路径
227-
let path = findPathAStar(layer, start, end);
228-
console.log('Raw path found:', path?.length, 'nodes');
229-
230-
if (path && path.length > 1) {
231-
// 2. 应用可见性平滑(与2D版本相同)
232-
const obstacles = renderer.roadNetData.obstacles || [];
233-
const metadata = renderer.roadNetData.metadata || {};
234-
235-
try {
236-
const smoothedPath = smoothPathVisibility(path, obstacles, {
237-
width: metadata.width || 0,
238-
height: metadata.height || 0,
239-
useSpatialIndex: true,
240-
maxLookahead: 100, // 增加前瞻距离,优先选择直线路径
241-
clearance: 2.0 // 保持2.0单位安全距离
242-
});
243-
244-
const smoothTime = performance.now() - smoothStartTime;
245-
console.log('Smoothed path:', smoothedPath.length, 'nodes (reduced from', path.length, ')');
246-
path = smoothedPath;
247-
248-
// 3. 计算路径统计
249-
let totalLength = 0;
250-
let turns = 0;
251-
for (let i = 0; i < path.length - 1; i++) {
252-
const dx = path[i + 1].x - path[i].x;
253-
const dy = path[i + 1].y - path[i].y;
254-
totalLength += Math.sqrt(dx * dx + dy * dy);
255-
256-
// 计算转折(方向变化)
257-
if (i > 0) {
258-
const dx1 = path[i].x - path[i - 1].x;
259-
const dy1 = path[i].y - path[i - 1].y;
260-
const dx2 = path[i + 1].x - path[i].x;
261-
const dy2 = path[i + 1].y - path[i].y;
262-
263-
const angle1 = Math.atan2(dy1, dx1);
264-
const angle2 = Math.atan2(dy2, dx2);
265-
const angleDiff = Math.abs(angle2 - angle1);
266-
267-
if (angleDiff > 0.1) turns++;
268-
}
269-
}
270-
271-
// 4. 更新UI统计
272-
const pathStatus = document.getElementById('path-status');
273-
const pathLength = document.getElementById('path-length');
274-
const pathNodes = document.getElementById('path-nodes');
275-
const pathTurns = document.getElementById('path-turns');
276-
const pathSmoothMs = document.getElementById('path-smooth-ms');
277-
278-
if (pathStatus) pathStatus.textContent = '已找到';
279-
if (pathLength) pathLength.textContent = `${totalLength.toFixed(2)} m`;
280-
if (pathNodes) pathNodes.textContent = path.length;
281-
if (pathTurns) pathTurns.textContent = turns;
282-
if (pathSmoothMs) pathSmoothMs.textContent = `${smoothTime.toFixed(1)} ms`;
283-
284-
} catch (e) {
285-
console.warn('Path smoothing failed, using raw path:', e);
286-
}
287-
288-
// 5. 绘制平滑后的路径
289-
renderer.drawPath(path);
290-
} else {
291-
console.warn('No path found');
292-
293-
// 更新UI为未找到
294-
const pathStatus = document.getElementById('path-status');
295-
if (pathStatus) pathStatus.textContent = '未找到';
296-
}
297-
}, 0);
298-
}
299-
}
300-
});
301-
302-
// 使用 ResizeObserver 监听 #pixi-canvas 实际尺寸变化,避免初次布局与自适应引发抖动
303-
try {
304-
const ro = new ResizeObserver((entries) => {
305-
for (const entry of entries) {
306-
const cr = entry.contentRect;
307-
const newW = Math.max(1, Math.round(cr.width));
308-
const newH = Math.max(1, Math.round(cr.height));
309-
if (this.appLastW !== newW || this.appLastH !== newH) {
310-
console.log(`[ResizeObserver] Container size changed: ${newW}x${newH}`);
311-
this.appLastW = newW;
312-
this.appLastH = newH;
313-
renderer.resize(newW, newH);
314-
}
315-
}
316-
});
317-
ro.observe(pixiContainer);
318-
this._pixiResizeObserver = ro;
319-
console.log('[ResizeObserver] Started observing container');
320-
} catch (e) {
321-
console.warn('[ResizeObserver] not available:', e);
322-
}
73+
// 初始化 Managers
74+
this.uiManager.init();
75+
this.appEventManager.init();
32376

32477
this.isInitialized = true;
32578
console.log('✅ Application initialized successfully');
@@ -508,96 +261,6 @@ class App {
508261
});
509262
}
510263

511-
/**
512-
* 设置缩放控制
513-
*/
514-
setupZoomControls() {
515-
const zoomInBtn = document.getElementById('zoom-in-btn');
516-
const zoomOutBtn = document.getElementById('zoom-out-btn');
517-
const zoomResetBtn = document.getElementById('zoom-reset-btn');
518-
519-
if (zoomInBtn) {
520-
zoomInBtn.addEventListener('click', () => {
521-
renderer.zoomIn();
522-
const vp = renderer.getViewportRect && renderer.getViewportRect();
523-
if (vp) console.debug('[Zoom] in, viewport=', vp);
524-
});
525-
} else {
526-
console.warn('[UI] #zoom-in-btn not found');
527-
}
528-
529-
if (zoomOutBtn) {
530-
zoomOutBtn.addEventListener('click', () => {
531-
renderer.zoomOut();
532-
const vp = renderer.getViewportRect && renderer.getViewportRect();
533-
if (vp) console.debug('[Zoom] out, viewport=', vp);
534-
});
535-
} else {
536-
console.warn('[UI] #zoom-out-btn not found');
537-
}
538-
539-
if (zoomResetBtn) {
540-
zoomResetBtn.addEventListener('click', () => {
541-
renderer.resetView();
542-
const vp = renderer.getViewportRect && renderer.getViewportRect();
543-
if (vp) console.debug('[Zoom] reset, viewport=', vp);
544-
});
545-
} else {
546-
console.warn('[UI] #zoom-reset-btn not found');
547-
}
548-
}
549-
550-
/**
551-
* 设置 UI 事件处理
552-
*/
553-
setupUIEventHandlers() {
554-
// 表单提交
555-
this.inputForm.onSubmit((values) => {
556-
console.log('📝 Form submitted:', values);
557-
this.handleGenerate(values);
558-
});
559-
560-
// 层切换
561-
this.layerControl.onLayerChange((layerIndex) => {
562-
console.log(`🔄 Switching to layer ${layerIndex}`);
563-
renderer.showLayer(layerIndex);
564-
this.layerControl.updateLayerInfo(this.roadNetData);
565-
});
566-
567-
// 显示所有层
568-
this.layerControl.onShowAll(() => {
569-
console.log('👀 Showing all layers');
570-
renderer.showLayer(null);
571-
});
572-
573-
// 窗口大小调整
574-
const pixiContainer = document.getElementById('pixi-canvas');
575-
let resizeTimeout;
576-
window.addEventListener('resize', () => {
577-
clearTimeout(resizeTimeout);
578-
resizeTimeout = setTimeout(() => {
579-
if (!document.fullscreenElement) {
580-
const w = pixiContainer.clientWidth;
581-
const h = pixiContainer.clientHeight;
582-
console.log(
583-
`[Resize][window] viewport=${window.innerWidth}x${window.innerHeight} pixiContainer=${w}x${h} -> resize(${w}x${h})`,
584-
);
585-
renderer.resize(w, h);
586-
}
587-
}, 250);
588-
});
589-
590-
// FPS 显示开关
591-
try {
592-
const fpsChk = document.getElementById('show-fps');
593-
if (fpsChk && typeof renderer.setFpsVisible === 'function') {
594-
fpsChk.addEventListener('change', () => {
595-
renderer.setFpsVisible(!!fpsChk.checked);
596-
});
597-
}
598-
} catch (_) {}
599-
}
600-
601264
/**
602265
* 处理生成请求
603266
*/

0 commit comments

Comments
 (0)