@@ -52,7 +52,8 @@ const fallbackLayerPalette = [
5252] ;
5353
5454// Populate display controls with layer checkboxes and visibility tree.
55- export function populateDisplayControls ( app , visibility , WebSocketTileLayer ,
55+ export function populateDisplayControls ( app , visibility , selectability ,
56+ WebSocketTileLayer ,
5657 techData , redrawAllLayers ,
5758 HeatMapTileLayer ) {
5859 if ( ! app . displayControlsEl ) return ;
@@ -93,12 +94,20 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
9394 // has more than one entry).
9495 let chipletModel = null ;
9596
96- // Restore saved hidden-layers set from previous session .
97+ // Restore saved hidden-layers and non-selectable-layers sets .
9798 let savedHiddenLayers = new Set ( ) ;
99+ let savedNonSelectableLayers = new Set ( ) ;
98100 try {
99101 const raw = getCookie ( 'or_hidden_layers' ) ;
100102 if ( raw ) savedHiddenLayers = new Set ( JSON . parse ( decodeURIComponent ( raw ) ) ) ;
101103 } catch ( _ ) { /* ignore */ }
104+ try {
105+ const raw = getCookie ( 'or_nonselectable_layers' ) ;
106+ if ( raw ) {
107+ savedNonSelectableLayers
108+ = new Set ( JSON . parse ( decodeURIComponent ( raw ) ) ) ;
109+ }
110+ } catch ( _ ) { /* ignore */ }
102111
103112 // Global counter so each layer (across the whole hierarchy) gets a unique
104113 // z-index and palette slot regardless of which chiplet it belongs to.
@@ -187,6 +196,26 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
187196 } ;
188197 }
189198
199+ // Parallel selectability model with the same node ids as layerSpec so
200+ // syncLayerSelDom() and buildLayerDOM() can pair each visibility node
201+ // with its selectability peer.
202+ function mirrorForSelectability ( node ) {
203+ if ( ! node . children || node . children . length === 0 ) {
204+ const name = node . data && node . data . name ;
205+ const selectable = name ? ! savedNonSelectableLayers . has ( name ) : true ;
206+ if ( selectable && name ) {
207+ app . selectableLayers . add ( name ) ;
208+ }
209+ return { id : node . id , data : { name } , checked : selectable } ;
210+ }
211+ return {
212+ id : node . id ,
213+ data : { name : node . data && node . data . name } ,
214+ children : node . children . map ( mirrorForSelectability ) ,
215+ } ;
216+ }
217+ const layerSelSpec = mirrorForSelectability ( layerSpec ) ;
218+
190219 const layerModel = new CheckboxTreeModel ( ( ) => {
191220 // Single pass over the tree: rebuild visibleLayerNames in place
192221 // (the WebSocketTileLayer closure captured this Set by reference
@@ -250,7 +279,8 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
250279 }
251280 }
252281 } ) ;
253-
282+ // Visibility off ⇒ selectability disabled — refresh selectability DOM.
283+ syncLayerSelDom ( ) ;
254284 // Refresh pins layer so it filters by the updated visible_layers.
255285 if ( app . pinsLayer && app . map . hasLayer ( app . pinsLayer ) ) {
256286 app . pinsLayer . refreshTiles ( ) ;
@@ -295,6 +325,40 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
295325 app . layerModel = layerModel ; // expose it so other rendering mechanism can use it
296326 layerModel . addFromSpec ( layerSpec ) ;
297327
328+ // Parallel selectability model — picks gate on this set on the server.
329+ const layerSelModel = new CheckboxTreeModel ( ( ) => {
330+ // Rebuild from scratch so multi-chiplet/multi-tech subtrees that
331+ // share a layer name don't fall into last-writer-wins (an unchecked
332+ // M1 leaf in one subtree would otherwise delete the name even when
333+ // another M1 leaf is still checked). Mutate in place — the
334+ // WebSocketTileLayer closure captured this Set by reference.
335+ app . selectableLayers . clear ( ) ;
336+ layerSelModel . forEach ( node => {
337+ if ( node . checked && node . data && node . data . name ) {
338+ app . selectableLayers . add ( node . data . name ) ;
339+ }
340+ } ) ;
341+ syncLayerSelDom ( ) ;
342+ const nonSel
343+ = techData . layers . filter ( n => ! app . selectableLayers . has ( n ) ) ;
344+ setCookie ( 'or_nonselectable_layers' ,
345+ encodeURIComponent ( JSON . stringify ( nonSel ) ) ) ;
346+ } ) ;
347+ layerSelModel . addFromSpec ( layerSelSpec ) ;
348+
349+ // Sync layer selectability DOM: visibility off disables the sel checkbox.
350+ function syncLayerSelDom ( ) {
351+ layerSelModel . forEach ( node => {
352+ if ( ! node . selCb ) return ;
353+ node . selCb . checked = node . checked ;
354+ node . selCb . indeterminate = node . indeterminate ;
355+ const visNode = layerModel . get ( node . id ) ;
356+ const visOff
357+ = visNode && ! visNode . checked && ! visNode . indeterminate ;
358+ node . selCb . disabled = visOff ;
359+ } ) ;
360+ }
361+
298362 // --- Layer context menu (right-click) ---
299363 const contextMenu = document . createElement ( 'div' ) ;
300364 contextMenu . className = 'context-menu' ;
@@ -330,19 +394,34 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
330394 } ) ;
331395
332396 function buildLayerDOM ( node , isRoot = false ) {
397+ const selNode = layerSelModel . get ( node . id ) ;
333398 if ( ! node . children || node . children . length === 0 ) {
334399 // Leaf node (layer)
335400 const label = document . createElement ( 'label' ) ;
336401
337402 const checkbox = document . createElement ( 'input' ) ;
338403 checkbox . type = 'checkbox' ;
404+ checkbox . title = 'Visible' ;
339405 checkbox . checked = node . checked ;
340406 node . cb = checkbox ;
341407 checkbox . addEventListener ( 'change' , ( ) => {
342408 layerModel . check ( node . id , checkbox . checked ) ;
343409 } ) ;
344410 label . appendChild ( checkbox ) ;
345411
412+ if ( selNode ) {
413+ const selCheckbox = document . createElement ( 'input' ) ;
414+ selCheckbox . type = 'checkbox' ;
415+ selCheckbox . className = 'vis-sel-cb' ;
416+ selCheckbox . title = 'Selectable' ;
417+ selCheckbox . checked = selNode . checked ;
418+ selNode . selCb = selCheckbox ;
419+ selCheckbox . addEventListener ( 'change' , ( ) => {
420+ layerSelModel . check ( node . id , selCheckbox . checked ) ;
421+ } ) ;
422+ label . appendChild ( selCheckbox ) ;
423+ }
424+
346425 const index = node . data . colorIndex ;
347426 const name = node . data . name ;
348427
@@ -390,6 +469,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
390469
391470 const cb = document . createElement ( 'input' ) ;
392471 cb . type = 'checkbox' ;
472+ cb . title = 'Visible' ;
393473 cb . checked = node . checked ;
394474 cb . indeterminate = node . indeterminate ;
395475 node . cb = cb ;
@@ -398,6 +478,20 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
398478 } ) ;
399479 header . appendChild ( cb ) ;
400480
481+ if ( selNode ) {
482+ const selCb = document . createElement ( 'input' ) ;
483+ selCb . type = 'checkbox' ;
484+ selCb . className = 'vis-sel-cb' ;
485+ selCb . title = 'Selectable' ;
486+ selCb . checked = selNode . checked ;
487+ selCb . indeterminate = selNode . indeterminate ;
488+ selNode . selCb = selCb ;
489+ selCb . addEventListener ( 'change' , ( ) => {
490+ layerSelModel . check ( node . id , selCb . checked ) ;
491+ } ) ;
492+ header . appendChild ( selCb ) ;
493+ }
494+
401495 const name = isRoot ? 'Layers' : ( node . data . name || 'Group' ) ;
402496 header . appendChild ( document . createTextNode ( name ) ) ;
403497 group . appendChild ( header ) ;
@@ -429,6 +523,10 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
429523
430524 app . displayControlsEl . appendChild ( layerGroup ) ;
431525
526+ // Initial selectability DOM sync (esp. disabled state for layers whose
527+ // visibility was restored as false).
528+ syncLayerSelDom ( ) ;
529+
432530 // --- Chiplets group (multi-die / 3D-IC visibility) ---
433531 //
434532 // Web-only feature: the Qt GUI has no equivalent panel today —
@@ -731,8 +829,10 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
731829 }
732830
733831 // --- Visibility tree (ordered to match Qt GUI display controls) ---
734- const visTree = new VisTree ( visibility , redrawAllLayers ) ;
735- visTree . add ( { label : 'Nets' , children : [
832+ // Subtrees that opt into a second "selectable" checkbox column mirror
833+ // the Qt GUI's selectability column (see displayControls.cpp).
834+ const visTree = new VisTree ( visibility , selectability , redrawAllLayers ) ;
835+ visTree . add ( { label : 'Nets' , addSelectable : true , children : [
736836 { key : 'net_signal' , label : 'Signal' } ,
737837 { key : 'net_power' , label : 'Power' } ,
738838 { key : 'net_ground' , label : 'Ground' } ,
@@ -742,7 +842,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
742842 { key : 'net_scan' , label : 'Scan' } ,
743843 { key : 'net_analog' , label : 'Analog' } ,
744844 ] } ) ;
745- visTree . add ( { label : 'Instances' , children : [
845+ visTree . add ( { label : 'Instances' , addSelectable : true , children : [
746846 { label : 'Std Cells' , visKey : 'stdcells' , disabled : ! app . hasLiberty , children : [
747847 { label : 'Bufs/Invs' , children : [
748848 { key : 'std_bufinv_timing' , label : 'Timing opt.' } ,
@@ -777,13 +877,13 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
777877 { key : 'phys_other' , label : 'Other' } ,
778878 ] } ,
779879 ] } ) ;
780- visTree . add ( { label : 'Blockages' , children : [
880+ visTree . add ( { label : 'Blockages' , addSelectable : true , children : [
781881 { key : 'placement_blockages' , label : 'Placement' } ,
782882 { key : 'routing_obstructions' , label : 'Routing' } ,
783883 ] } ) ;
784884 if ( techData . sites && techData . sites . length > 0 ) {
785- visTree . add ( { label : 'Rows' , visKey : 'rows' , children :
786- techData . sites . map ( name => ( {
885+ visTree . add ( { label : 'Rows' , visKey : 'rows' , addSelectable : true ,
886+ children : techData . sites . map ( name => ( {
787887 key : 'site_' + name , label : name ,
788888 } ) ) ,
789889 } ) ;
@@ -801,13 +901,13 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
801901 { key : 'srouting_segments' , label : 'Segments' } ,
802902 { key : 'srouting_vias' , label : 'Vias' } ,
803903 ] } ,
804- { key : 'pins' , label : 'Pins' } ,
904+ { key : 'pins' , label : 'Pins' , selectable : true } ,
805905 { key : 'pin_names' , label : 'Pin Names' , disabledBy : 'pins' } ,
806906 ] } ) ;
807907 visTree . add ( { label : 'Misc' , children : [
808908 { label : 'Instances' , children : [
809909 { key : 'inst_names' , label : 'Names' } ,
810- { key : 'inst_pins' , label : 'Pins' } ,
910+ { key : 'inst_pins' , label : 'Pins' , selectable : true } ,
811911 { key : 'inst_pin_names' , label : 'Pin Names' , disabledBy : 'inst_pins' } ,
812912 { key : 'blockages' , label : 'Blockages' } ,
813913 ] } ,
0 commit comments