Skip to content

Commit e281635

Browse files
authored
Merge pull request #10601 from The-OpenROAD-Project-staging/web-multi-select
web: add multi-selection and inspector cycling with pulse animation
2 parents a18be6b + e5d8cc3 commit e281635

7 files changed

Lines changed: 760 additions & 8 deletions

File tree

src/web/src/inspector.js

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5365
export 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
}

src/web/src/main.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ const inspector = createInspectorPanel(app, redrawAllLayers);
629629
const createInspector = inspector.createInspector;
630630
const updateInspector = inspector.updateInspector;
631631
const highlightBBox = inspector.highlightBBox;
632+
const pulseHighlight = inspector.pulseHighlight;
632633
app.updateInspector = updateInspector;
633634
app.navigateInspector = inspector.navigateInspector;
634635

@@ -1078,6 +1079,9 @@ app.websocketManager.readyPromise.then(async () => {
10781079
selectable_layers: [...app.selectableLayers],
10791080
...vf,
10801081
};
1082+
if (e.originalEvent && e.originalEvent.shiftKey) {
1083+
selectRequest.add_to_selection = true;
1084+
}
10811085
if (app.visibleChiplets instanceof Set) {
10821086
selectRequest.visible_chiplets = [...app.visibleChiplets];
10831087
}
@@ -1099,8 +1103,12 @@ app.websocketManager.readyPromise.then(async () => {
10991103
if (inst.bbox) {
11001104
highlightBBox(inst.bbox[0], inst.bbox[1],
11011105
inst.bbox[2], inst.bbox[3]);
1106+
pulseHighlight(inst.bbox);
11021107
}
1103-
} else {
1108+
} else if (!selectRequest.add_to_selection) {
1109+
// Shift+click on empty space preserves the existing
1110+
// multi-selection on the server, so leave the
1111+
// inspector/navigation controls and highlight intact.
11041112
updateInspector(null);
11051113
if (app.highlightRect) {
11061114
app.map.removeLayer(app.highlightRect);

0 commit comments

Comments
 (0)