Skip to content

Commit 1450298

Browse files
committed
web: add selectability column to display controls
Mirrors the Qt GUI's display-controls "selectable" column in the web viewer: a second checkbox per row alongside the visibility checkbox. Picks (selectAt) require both visible AND selectable; rendering is unchanged. - TileVisibility gains *_selectable peers for every Qt-parity flag, plus selectable_layers / site_selectable maps. Instance classification is factored into a shared classifyInstance() so isInstVisible and the new isInstSelectable cannot drift. - parseFromJson reads s_* keys, selectable_layers, and s_site_<name>. - selectAt() gates instance picks by isInstSelectable, net picks by isNetSelectable, layer iteration by isLayerSelectable, and BTerm picks by pins_selectable. - VisTree renders a second checkbox column when a leaf opts in with selectable:true or inherits addSelectable from an ancestor group. Visibility unchecked auto-disables the selectability checkbox. Node ids are path-qualified to avoid collisions between same-labeled subtrees (e.g. top-level Instances vs Misc / Instances). - display-controls layer subtree gets a parallel layerSelModel and a second checkbox per layer, persisted to or_nonselectable_layers. - main.js initializes a selectability object (Qt defaults true) and app.selectableLayers, persisted to or_selectability, and includes s_* + selectable_layers on every tile/select request. - New JS tests cover the second column, tri-state propagation, visibility-off-disables-selectability, and the same-label-collision regression. New C++ tests cover parseFromJson, the new helpers, and selectAt gating by instance and layer selectability. Signed-off-by: Matt Liberty <mliberty@precisioninno.com>
1 parent b53c665 commit 1450298

9 files changed

Lines changed: 990 additions & 84 deletions

File tree

src/web/src/display-controls.js

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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,39 @@ 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+
layerSelModel.forEach(node => {
331+
if (!node.data) return;
332+
if (node.data.name) {
333+
if (node.checked) {
334+
app.selectableLayers.add(node.data.name);
335+
} else {
336+
app.selectableLayers.delete(node.data.name);
337+
}
338+
}
339+
});
340+
syncLayerSelDom();
341+
const nonSel
342+
= techData.layers.filter(n => !app.selectableLayers.has(n));
343+
setCookie('or_nonselectable_layers',
344+
encodeURIComponent(JSON.stringify(nonSel)));
345+
});
346+
layerSelModel.addFromSpec(layerSelSpec);
347+
348+
// Sync layer selectability DOM: visibility off disables the sel checkbox.
349+
function syncLayerSelDom() {
350+
layerSelModel.forEach(node => {
351+
if (!node.selCb) return;
352+
node.selCb.checked = node.checked;
353+
node.selCb.indeterminate = node.indeterminate;
354+
const visNode = layerModel.get(node.id);
355+
const visOff
356+
= visNode && !visNode.checked && !visNode.indeterminate;
357+
node.selCb.disabled = visOff;
358+
});
359+
}
360+
298361
// --- Layer context menu (right-click) ---
299362
const contextMenu = document.createElement('div');
300363
contextMenu.className = 'context-menu';
@@ -330,19 +393,34 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
330393
});
331394

332395
function buildLayerDOM(node, isRoot = false) {
396+
const selNode = layerSelModel.get(node.id);
333397
if (!node.children || node.children.length === 0) {
334398
// Leaf node (layer)
335399
const label = document.createElement('label');
336400

337401
const checkbox = document.createElement('input');
338402
checkbox.type = 'checkbox';
403+
checkbox.title = 'Visible';
339404
checkbox.checked = node.checked;
340405
node.cb = checkbox;
341406
checkbox.addEventListener('change', () => {
342407
layerModel.check(node.id, checkbox.checked);
343408
});
344409
label.appendChild(checkbox);
345410

411+
if (selNode) {
412+
const selCheckbox = document.createElement('input');
413+
selCheckbox.type = 'checkbox';
414+
selCheckbox.className = 'vis-sel-cb';
415+
selCheckbox.title = 'Selectable';
416+
selCheckbox.checked = selNode.checked;
417+
selNode.selCb = selCheckbox;
418+
selCheckbox.addEventListener('change', () => {
419+
layerSelModel.check(node.id, selCheckbox.checked);
420+
});
421+
label.appendChild(selCheckbox);
422+
}
423+
346424
const index = node.data.colorIndex;
347425
const name = node.data.name;
348426

@@ -390,6 +468,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
390468

391469
const cb = document.createElement('input');
392470
cb.type = 'checkbox';
471+
cb.title = 'Visible';
393472
cb.checked = node.checked;
394473
cb.indeterminate = node.indeterminate;
395474
node.cb = cb;
@@ -398,6 +477,20 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
398477
});
399478
header.appendChild(cb);
400479

480+
if (selNode) {
481+
const selCb = document.createElement('input');
482+
selCb.type = 'checkbox';
483+
selCb.className = 'vis-sel-cb';
484+
selCb.title = 'Selectable';
485+
selCb.checked = selNode.checked;
486+
selCb.indeterminate = selNode.indeterminate;
487+
selNode.selCb = selCb;
488+
selCb.addEventListener('change', () => {
489+
layerSelModel.check(node.id, selCb.checked);
490+
});
491+
header.appendChild(selCb);
492+
}
493+
401494
const name = isRoot ? 'Layers' : (node.data.name || 'Group');
402495
header.appendChild(document.createTextNode(name));
403496
group.appendChild(header);
@@ -429,6 +522,10 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
429522

430523
app.displayControlsEl.appendChild(layerGroup);
431524

525+
// Initial selectability DOM sync (esp. disabled state for layers whose
526+
// visibility was restored as false).
527+
syncLayerSelDom();
528+
432529
// --- Chiplets group (multi-die / 3D-IC visibility) ---
433530
//
434531
// Web-only feature: the Qt GUI has no equivalent panel today —
@@ -731,8 +828,10 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
731828
}
732829

733830
// --- Visibility tree (ordered to match Qt GUI display controls) ---
734-
const visTree = new VisTree(visibility, redrawAllLayers);
735-
visTree.add({ label: 'Nets', children: [
831+
// Subtrees that opt into a second "selectable" checkbox column mirror
832+
// the Qt GUI's selectability column (see displayControls.cpp).
833+
const visTree = new VisTree(visibility, selectability, redrawAllLayers);
834+
visTree.add({ label: 'Nets', addSelectable: true, children: [
736835
{ key: 'net_signal', label: 'Signal' },
737836
{ key: 'net_power', label: 'Power' },
738837
{ key: 'net_ground', label: 'Ground' },
@@ -742,7 +841,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
742841
{ key: 'net_scan', label: 'Scan' },
743842
{ key: 'net_analog', label: 'Analog' },
744843
]});
745-
visTree.add({ label: 'Instances', children: [
844+
visTree.add({ label: 'Instances', addSelectable: true, children: [
746845
{ label: 'Std Cells', visKey: 'stdcells', disabled: !app.hasLiberty, children: [
747846
{ label: 'Bufs/Invs', children: [
748847
{ key: 'std_bufinv_timing', label: 'Timing opt.' },
@@ -777,13 +876,13 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
777876
{ key: 'phys_other', label: 'Other' },
778877
]},
779878
]});
780-
visTree.add({ label: 'Blockages', children: [
879+
visTree.add({ label: 'Blockages', addSelectable: true, children: [
781880
{ key: 'placement_blockages', label: 'Placement' },
782881
{ key: 'routing_obstructions', label: 'Routing' },
783882
]});
784883
if (techData.sites && techData.sites.length > 0) {
785-
visTree.add({ label: 'Rows', visKey: 'rows', children:
786-
techData.sites.map(name => ({
884+
visTree.add({ label: 'Rows', visKey: 'rows', addSelectable: true,
885+
children: techData.sites.map(name => ({
787886
key: 'site_' + name, label: name,
788887
})),
789888
});
@@ -801,13 +900,13 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer,
801900
{ key: 'srouting_segments', label: 'Segments' },
802901
{ key: 'srouting_vias', label: 'Vias' },
803902
]},
804-
{ key: 'pins', label: 'Pins' },
903+
{ key: 'pins', label: 'Pins', selectable: true },
805904
{ key: 'pin_names', label: 'Pin Names', disabledBy: 'pins' },
806905
]});
807906
visTree.add({ label: 'Misc', children: [
808907
{ label: 'Instances', children: [
809908
{ key: 'inst_names', label: 'Names' },
810-
{ key: 'inst_pins', label: 'Pins' },
909+
{ key: 'inst_pins', label: 'Pins', selectable: true },
811910
{ key: 'inst_pin_names', label: 'Pin Names', disabledBy: 'inst_pins' },
812911
{ key: 'blockages', label: 'Blockages' },
813912
]},

src/web/src/main.js

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const app = {
103103
// "render every chiplet" (single-chip designs).
104104
visibleChiplets: null,
105105
useTrueZ: getCookie('or_use_true_z') === '1',
106+
selectableLayers: new Set(),
106107
heatMapData: null,
107108
activeHeatMap: '',
108109
heatMapLayer: null,
@@ -194,11 +195,67 @@ try {
194195
// Ignore malformed cookie.
195196
}
196197

198+
// Selectability mirrors the Qt GUI's display-controls "selectable" column.
199+
// Defaults to true (everything selectable), matching the Qt GUI. Only
200+
// categories that the Qt GUI exposes a selectable checkbox for are listed
201+
// here; the server treats unspecified keys as selectable.
202+
const selectability = {
203+
stdcells: true,
204+
macros: true,
205+
pad_input: true,
206+
pad_output: true,
207+
pad_inout: true,
208+
pad_power: true,
209+
pad_spacer: true,
210+
pad_areaio: true,
211+
pad_other: true,
212+
phys_fill: true,
213+
phys_endcap: true,
214+
phys_welltap: true,
215+
phys_tie: true,
216+
phys_antenna: true,
217+
phys_cover: true,
218+
phys_bump: true,
219+
phys_other: true,
220+
std_bufinv: true,
221+
std_bufinv_timing: true,
222+
std_clock_bufinv: true,
223+
std_clock_gate: true,
224+
std_level_shift: true,
225+
std_sequential: true,
226+
std_combinational: true,
227+
net_signal: true,
228+
net_power: true,
229+
net_ground: true,
230+
net_clock: true,
231+
net_reset: true,
232+
net_tieoff: true,
233+
net_scan: true,
234+
net_analog: true,
235+
pins: true,
236+
inst_pins: true,
237+
placement_blockages: true,
238+
routing_obstructions: true,
239+
};
240+
241+
try {
242+
const saved = getCookie('or_selectability');
243+
if (saved) {
244+
const parsed = JSON.parse(decodeURIComponent(saved));
245+
for (const [k, v] of Object.entries(parsed)) {
246+
selectability[k] = !!v;
247+
}
248+
}
249+
} catch (_) {
250+
// Ignore malformed cookie.
251+
}
252+
197253
// `app` is forwarded so the tile layer can read app.visibleChiplets
198254
// lazily on every request — the field is populated by display-controls
199255
// once the server's tech metadata arrives.
200-
const WebSocketTileLayer
201-
= createWebSocketTileLayer(visibility, app.visibleLayerNames, app);
256+
const WebSocketTileLayer = createWebSocketTileLayer(
257+
visibility, app.visibleLayerNames, selectability, app.selectableLayers,
258+
app);
202259
const BLANK_TILE
203260
= 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
204261

@@ -301,8 +358,11 @@ function updateHeatMaps(data) {
301358
app.updateHeatMaps = updateHeatMaps;
302359

303360
function redrawAllLayers() {
304-
// Persist visibility state to cookie so it survives page reloads.
361+
// Persist visibility and selectability state to cookies so they survive
362+
// page reloads.
305363
setCookie('or_visibility', encodeURIComponent(JSON.stringify(visibility)));
364+
setCookie('or_selectability',
365+
encodeURIComponent(JSON.stringify(selectability)));
306366

307367
// Show/hide modules layer based on module_view visibility
308368
if (app.modulesLayer) {
@@ -987,12 +1047,18 @@ app.websocketManager.readyPromise.then(async () => {
9871047
for (const [k, v] of Object.entries(visibility)) {
9881048
vf[k] = !!v;
9891049
}
1050+
// Selectability is sent with `s_` prefix to mirror the flat
1051+
// visibility key scheme; the server parses both columns.
1052+
for (const [k, v] of Object.entries(selectability)) {
1053+
vf['s_' + k] = !!v;
1054+
}
9901055
const selectRequest = {
9911056
type: 'select',
9921057
dbu_x,
9931058
dbu_y,
9941059
zoom: Math.round(app.map.getZoom()),
9951060
visible_layers: [...app.visibleLayerNames],
1061+
selectable_layers: [...app.selectableLayers],
9961062
...vf,
9971063
};
9981064
if (app.visibleChiplets instanceof Set) {
@@ -1092,7 +1158,8 @@ app.websocketManager.readyPromise.then(async () => {
10921158
});
10931159
}
10941160

1095-
populateDisplayControls(app, visibility, WebSocketTileLayer,
1161+
populateDisplayControls(app, visibility, selectability,
1162+
WebSocketTileLayer,
10961163
techData, redrawAllLayers, HeatMapTileLayer);
10971164
updateHeatMaps(heatMapData);
10981165

src/web/src/style.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,29 @@ html, body {
677677
pointer-events: none;
678678
}
679679

680+
/* Selectability checkbox (second column, next to the visibility checkbox).
681+
* Small left margin gives a clear visual separation between the two columns;
682+
* disabled state (visibility off) dims the checkbox. */
683+
.display-controls .vis-sel-cb {
684+
margin-left: 2px;
685+
margin-right: 2px;
686+
}
687+
.display-controls .vis-sel-cb.vis-sel-spacer {
688+
/* Layout-only placeholder when a row has no selectability checkbox. */
689+
display: inline-block;
690+
width: 13px;
691+
height: 13px;
692+
visibility: hidden;
693+
}
694+
.display-controls .vis-sel-cb:disabled {
695+
opacity: 0.35;
696+
cursor: not-allowed;
697+
}
698+
.display-controls label.vis-sel-disabled {
699+
/* Visually dim labels whose selectability is disabled by visibility. */
700+
color: var(--fg-muted);
701+
}
702+
680703
/* Layer context menu */
681704
.context-menu {
682705
position: fixed;

0 commit comments

Comments
 (0)