@@ -9,12 +9,9 @@ import InputForm from './components/InputForm.js';
99import ProgressBar from './components/ProgressBar.js' ;
1010import LayerControl from './components/LayerControl.js' ;
1111import statusManager from './utils/statusManager.js' ;
12- import exportManager from './utils/exportManager.js' ;
1312import 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