@@ -50,6 +50,18 @@ const CLEAR_FOCUS_SVG =
5050 '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>' +
5151 '</svg>' ;
5252
53+ // Chevron left: Material "chevron_left"
54+ const CHEVRON_LEFT_SVG =
55+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">' +
56+ '<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>' +
57+ '</svg>' ;
58+
59+ // Chevron right: Material "chevron_right"
60+ const CHEVRON_RIGHT_SVG =
61+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">' +
62+ '<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>' +
63+ '</svg>' ;
64+
5365export function createInspectorPanel ( app , redrawAllLayers ) {
5466 let lastInspectData = null ;
5567 let pendingInspectId = null ;
@@ -216,6 +228,56 @@ export function createInspectorPanel(app, redrawAllLayers) {
216228 } ) ;
217229 }
218230
231+ function cycleSelection ( direction ) {
232+ const reqType = direction > 0 ? 'select_next' : 'select_prev' ;
233+ if ( pendingInspectId !== null ) {
234+ app . websocketManager . cancel ( pendingInspectId ) ;
235+ pendingInspectId = null ;
236+ }
237+ showLoading ( ) ;
238+ const promise = app . websocketManager . request ( { type : reqType } ) ;
239+ pendingInspectId = promise . requestId ;
240+ promise
241+ . then ( data => {
242+ pendingInspectId = null ;
243+ if ( data . error ) {
244+ console . error ( 'Selection cycle error:' , data . error ) ;
245+ updateInspector ( lastInspectData ) ;
246+ return ;
247+ }
248+ updateInspector ( data ) ;
249+ // Keep schematic in sync when cycling to an instance.
250+ if ( data . type === 'Inst' && data . name ) {
251+ app . selectedInstanceName = data . name ;
252+ if ( app . schematicWidget ) {
253+ app . schematicWidget . refresh ( ) ;
254+ }
255+ }
256+ if ( app . map ) {
257+ app . map . closePopup ( ) ;
258+ }
259+ clearClientHoverHighlight ( ) ;
260+ if ( app . highlightRect && app . map ) {
261+ app . map . removeLayer ( app . highlightRect ) ;
262+ app . highlightRect = null ;
263+ }
264+ if ( data . bbox && app . map && app . designScale ) {
265+ const [ x1 , y1 , x2 , y2 ] = data . bbox ;
266+ if ( data . type !== 'Inst' ) {
267+ highlightBBox ( x1 , y1 , x2 , y2 ) ;
268+ }
269+ pulseHighlight ( data . bbox ) ;
270+ }
271+ // Redraw tiles to restore selection-set highlights.
272+ redrawAllLayers ( ) ;
273+ } )
274+ . catch ( err => {
275+ pendingInspectId = null ;
276+ console . error ( 'Selection cycle failed:' , err ) ;
277+ updateInspector ( lastInspectData ) ;
278+ } ) ;
279+ }
280+
219281 function navigateInspector ( selectId ) {
220282 // Cancel previous in-flight inspect request
221283 if ( pendingInspectId !== null ) {
@@ -252,6 +314,7 @@ export function createInspectorPanel(app, redrawAllLayers) {
252314 if ( data . type !== 'Inst' ) {
253315 highlightBBox ( x1 , y1 , x2 , y2 ) ;
254316 }
317+ pulseHighlight ( data . bbox ) ;
255318 }
256319 // Redraw tiles to update instance highlight
257320 redrawAllLayers ( ) ;
@@ -288,9 +351,12 @@ export function createInspectorPanel(app, redrawAllLayers) {
288351 app . map . removeLayer ( app . highlightRect ) ;
289352 app . highlightRect = null ;
290353 }
291- if ( data . bbox && app . map && app . designScale && data . type !== 'Inst' ) {
292- const [ x1 , y1 , x2 , y2 ] = data . bbox ;
293- highlightBBox ( x1 , y1 , x2 , y2 ) ;
354+ if ( data . bbox && app . map && app . designScale ) {
355+ if ( data . type !== 'Inst' ) {
356+ const [ x1 , y1 , x2 , y2 ] = data . bbox ;
357+ highlightBBox ( x1 , y1 , x2 , y2 ) ;
358+ }
359+ pulseHighlight ( data . bbox ) ;
294360 }
295361 redrawAllLayers ( ) ;
296362 } )
@@ -310,6 +376,44 @@ export function createInspectorPanel(app, redrawAllLayers) {
310376 } ) . addTo ( app . map ) ;
311377 }
312378
379+ // Briefly pulse the object's bbox so the user can see which object
380+ // the inspector is now showing — mirrors the Qt GUI's selection
381+ // animation. The pulse is a filled rectangle that fades in and out
382+ // several times, then removes itself.
383+ let pulseLayer = null ;
384+ function pulseHighlight ( bbox ) {
385+ if ( ! bbox || ! app . map || ! app . designScale ) return ;
386+ if ( pulseLayer ) {
387+ app . map . removeLayer ( pulseLayer ) ;
388+ pulseLayer = null ;
389+ }
390+ const [ x1 , y1 , x2 , y2 ] = bbox ;
391+ const bounds = dbuRectToBounds (
392+ x1 , y1 , x2 , y2 , app . designScale , app . designMaxDXDY ,
393+ app . designOriginX , app . designOriginY ) ;
394+ pulseLayer = L . rectangle ( bounds , {
395+ color : '#ff0' ,
396+ weight : 3 ,
397+ fill : true ,
398+ fillColor : '#ff0' ,
399+ fillOpacity : 0.25 ,
400+ opacity : 1 ,
401+ interactive : false ,
402+ className : 'selection-pulse' ,
403+ pane : app . hoverHighlightPane ,
404+ } ) . addTo ( app . map ) ;
405+ // Remove after the animation finishes (3 cycles × 350ms = 1050ms).
406+ const layerToRemove = pulseLayer ;
407+ setTimeout ( ( ) => {
408+ if ( layerToRemove && app . map && app . map . hasLayer ( layerToRemove ) ) {
409+ app . map . removeLayer ( layerToRemove ) ;
410+ }
411+ if ( pulseLayer === layerToRemove ) {
412+ pulseLayer = null ;
413+ }
414+ } , 1100 ) ;
415+ }
416+
313417 function renderProperty ( prop , data ) {
314418 // Group with children (PropertyList or SelectionSet)
315419 if ( prop . children ) {
@@ -477,6 +581,36 @@ export function createInspectorPanel(app, redrawAllLayers) {
477581 app . inspectorEl . appendChild ( toolbar ) ;
478582 }
479583
584+ // Selection navigation bar (visible when multiple objects selected)
585+ const selCount = data . selection_count || 0 ;
586+ const selIndex = typeof data . selection_index === 'number'
587+ ? data . selection_index : - 1 ;
588+ if ( selCount > 1 && selIndex >= 0 ) {
589+ const nav = document . createElement ( 'div' ) ;
590+ nav . className = 'inspector-selection-nav' ;
591+
592+ const prevBtn = document . createElement ( 'button' ) ;
593+ prevBtn . className = 'inspector-btn' ;
594+ prevBtn . title = 'Previous (Shift+click to multi-select)' ;
595+ prevBtn . innerHTML = CHEVRON_LEFT_SVG ;
596+ prevBtn . addEventListener ( 'click' , ( ) => cycleSelection ( - 1 ) ) ;
597+
598+ const label = document . createElement ( 'span' ) ;
599+ label . className = 'inspector-selection-label' ;
600+ label . textContent = ( selIndex + 1 ) + ' / ' + selCount ;
601+
602+ const nextBtn = document . createElement ( 'button' ) ;
603+ nextBtn . className = 'inspector-btn' ;
604+ nextBtn . title = 'Next' ;
605+ nextBtn . innerHTML = CHEVRON_RIGHT_SVG ;
606+ nextBtn . addEventListener ( 'click' , ( ) => cycleSelection ( + 1 ) ) ;
607+
608+ nav . appendChild ( prevBtn ) ;
609+ nav . appendChild ( label ) ;
610+ nav . appendChild ( nextBtn ) ;
611+ app . inspectorEl . appendChild ( nav ) ;
612+ }
613+
480614 for ( const prop of data . properties ) {
481615 app . inspectorEl . appendChild ( renderProperty ( prop , data ) ) ;
482616 }
@@ -492,5 +626,5 @@ export function createInspectorPanel(app, redrawAllLayers) {
492626 updateInspector ( null ) ;
493627 }
494628
495- return { createInspector, updateInspector, highlightBBox, navigateInspector } ;
629+ return { createInspector, updateInspector, highlightBBox, pulseHighlight , navigateInspector } ;
496630}
0 commit comments