Update display controls layers in the WebGUI#10647
Conversation
Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a 'Layer Config' modal dialog to the web viewer, enabling users to customize layer colors, fill patterns, and opacity, bringing it to structural parity with the Qt GUI. It also adds support for rendering metal fills and groups layers by type (Backside, Implant, Other) in the layer tree. The review feedback highlights several key improvement opportunities: enhancing robustness when parsing cookies by validating the JSON structure, mitigating a potential XSS vulnerability in the modal dialog by avoiding direct HTML interpolation of layer names, and optimizing rendering performance in TileGenerator by unswitching loops to avoid redundant, loop-invariant pattern checks.
| try { | ||
| const raw = getCookie('or_layer_patterns'); | ||
| if (raw) savedLayerPatterns = JSON.parse(decodeURIComponent(raw)); | ||
| } catch (_) { /* ignore */ } | ||
| try { | ||
| const raw = getCookie('or_layer_colors'); | ||
| if (raw) savedLayerColors = JSON.parse(decodeURIComponent(raw)); | ||
| } catch (_) { /* ignore */ } | ||
| try { | ||
| const raw = getCookie('or_layer_opacity'); | ||
| if (raw) savedLayerOpacity = JSON.parse(decodeURIComponent(raw)); | ||
| } catch (_) { /* ignore */ } |
There was a problem hiding this comment.
Parsing cookies directly with JSON.parse without validating the type of the returned value can lead to runtime TypeErrors if the cookie is corrupted or set to a non-object value (e.g., "null" or a number). It is safer to use a helper function that validates that the parsed result is a non-null object.
| try { | |
| const raw = getCookie('or_layer_patterns'); | |
| if (raw) savedLayerPatterns = JSON.parse(decodeURIComponent(raw)); | |
| } catch (_) { /* ignore */ } | |
| try { | |
| const raw = getCookie('or_layer_colors'); | |
| if (raw) savedLayerColors = JSON.parse(decodeURIComponent(raw)); | |
| } catch (_) { /* ignore */ } | |
| try { | |
| const raw = getCookie('or_layer_opacity'); | |
| if (raw) savedLayerOpacity = JSON.parse(decodeURIComponent(raw)); | |
| } catch (_) { /* ignore */ } | |
| const parseCookieObject = (name) => { | |
| try { | |
| const raw = getCookie(name); | |
| if (raw) { | |
| const parsed = JSON.parse(decodeURIComponent(raw)); | |
| if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { | |
| return parsed; | |
| } | |
| } | |
| } catch (_) {} | |
| return {}; | |
| }; | |
| savedLayerPatterns = parseCookieObject('or_layer_patterns'); | |
| savedLayerColors = parseCookieObject('or_layer_colors'); | |
| savedLayerOpacity = parseCookieObject('or_layer_opacity'); |
| const { dialog, close } = createModalDialog(` | ||
| <div class="modal-dialog layer-config-dialog"> | ||
| <h3>Layer Config — ${node.data.name}</h3> | ||
| <div class="layer-config-row"> | ||
| <label for="lc-color">Color</label> | ||
| <input type="color" id="lc-color" value="${rgbToHex(curColor)}"> | ||
| </div> | ||
| <div class="layer-config-row"> | ||
| <label for="lc-pattern">Fill pattern</label> | ||
| <select id="lc-pattern">${patternOptions}</select> | ||
| </div> | ||
| <div class="layer-config-row"> | ||
| <label for="lc-opacity">Opacity</label> | ||
| <input type="range" id="lc-opacity" min="0" max="100" | ||
| value="${Math.round(curOpacity * 100)}"> | ||
| <span id="lc-opacity-val">${Math.round(curOpacity * 100)}%</span> | ||
| </div> | ||
| <div class="modal-buttons"> | ||
| <button id="lc-cancel">Cancel</button> | ||
| <button id="lc-ok" class="primary">OK</button> | ||
| </div> | ||
| </div>`); |
There was a problem hiding this comment.
Interpolating node.data.name directly into the HTML string passed to createModalDialog can lead to Cross-Site Scripting (XSS) vulnerabilities if a design file contains a maliciously crafted layer name. It is safer to use textContent to set the title dynamically.
const { dialog, close } = createModalDialog(
'<div class="modal-dialog layer-config-dialog">' +
'<h3 id="lc-title"></h3>' +
'<div class="layer-config-row">' +
' <label for="lc-color">Color</label>' +
' <input type="color" id="lc-color" value="' + rgbToHex(curColor) + '">' +
'</div>' +
'<div class="layer-config-row">' +
' <label for="lc-pattern">Fill pattern</label>' +
' <select id="lc-pattern">' + patternOptions + '</select>' +
'</div>' +
'<div class="layer-config-row">' +
' <label for="lc-opacity">Opacity</label>' +
' <input type="range" id="lc-opacity" min="0" max="100" value="' + Math.round(curOpacity * 100) + '">' +
' <span id="lc-opacity-val">' + Math.round(curOpacity * 100) + '%</span>' +
'</div>' +
'<div class="modal-buttons">' +
' <button id="lc-cancel">Cancel</button>' +
' <button id="lc-ok" class="primary">OK</button>' +
'</div>' +
'</div>'
);
dialog.querySelector('#lc-title').textContent = 'Layer Config — ' + node.data.name;| for (int iy = rect.yMin(); iy < rect.yMax(); ++iy) { | ||
| const int draw_y = (255 - iy); | ||
| const int gy = px_origin_y + draw_y; | ||
| for (int ix = rect.xMin(); ix < rect.xMax(); ++ix) { | ||
| const int draw_y = (255 - iy); | ||
| if (pattern != FillPattern::kSolid | ||
| && !patternMask(pattern, px_origin_x + ix, gy)) { | ||
| continue; | ||
| } | ||
| setPixel(buffer, ix, draw_y, color); | ||
| } | ||
| } |
There was a problem hiding this comment.
Checking pattern != FillPattern::kSolid inside the inner loop is inefficient because pattern is loop-invariant. Since kSolid is the default and most common case, unswitching the loop to separate the solid fill from the patterned fill will improve rendering performance.
| for (int iy = rect.yMin(); iy < rect.yMax(); ++iy) { | |
| const int draw_y = (255 - iy); | |
| const int gy = px_origin_y + draw_y; | |
| for (int ix = rect.xMin(); ix < rect.xMax(); ++ix) { | |
| const int draw_y = (255 - iy); | |
| if (pattern != FillPattern::kSolid | |
| && !patternMask(pattern, px_origin_x + ix, gy)) { | |
| continue; | |
| } | |
| setPixel(buffer, ix, draw_y, color); | |
| } | |
| } | |
| if (pattern == FillPattern::kSolid) { | |
| for (int iy = rect.yMin(); iy < rect.yMax(); ++iy) { | |
| const int draw_y = (255 - iy); | |
| for (int ix = rect.xMin(); ix < rect.xMax(); ++ix) { | |
| setPixel(buffer, ix, draw_y, color); | |
| } | |
| } | |
| } else { | |
| for (int iy = rect.yMin(); iy < rect.yMax(); ++iy) { | |
| const int draw_y = (255 - iy); | |
| const int gy = px_origin_y + draw_y; | |
| for (int ix = rect.xMin(); ix < rect.xMax(); ++ix) { | |
| if (!patternMask(pattern, px_origin_x + ix, gy)) { | |
| continue; | |
| } | |
| setPixel(buffer, ix, draw_y, color); | |
| } | |
| } | |
| } |
| if (pattern != FillPattern::kSolid | ||
| && !patternMask(pattern, px_origin_x + ix, gy)) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Checking pattern != FillPattern::kSolid inside the inner loop is inefficient because pattern is loop-invariant. Unswitching the loop for the solid fill case (which is the default and most common case) will avoid redundant checks and allow the compiler to better optimize the loop.
Example optimization:
if (pattern == FillPattern::kSolid) {
for (int ix = ix_min; ix < ix_max; ++ix) {
if (blend) {
blendPixel(image, ix, draw_y, color);
} else {
setPixel(image, ix, draw_y, color);
}
}
} else {
for (int ix = ix_min; ix < ix_max; ++ix) {
if (patternMask(pattern, px_origin_x + ix, gy)) {
if (blend) {
blendPixel(image, ix, draw_y, color);
} else {
setPixel(image, ix, draw_y, color);
}
}
}
}… fill loops - display-controls.js: parse the per-layer cookies (or_layer_patterns/ colors/opacity) through a parseCookieObject() helper that rejects non-object JSON, avoiding a TypeError on a corrupted cookie. - display-controls.js: set the Layer Config dialog title via textContent instead of HTML interpolation to avoid XSS from a layer name. - tile_generator.cpp: unswitch the pattern check out of the per-pixel inner loop in drawFilledRect() and fillPolygon() so the common solid fill skips the loop-invariant patternMask test. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>

Addresses a portion of #10619
Three blocks of functionality, all in OpenROAD's src/web module, closing gaps from table 2.2 ("Display controls — layers") and related areas:
Fill pattern per layer + Implant/Other categorization (❌/🟡 gaps in table 2.2)
"Layer Config" dialog (color + pattern + opacity) — parity with the GUI's DisplayColorDialog
Geometry rasterization for implant/other + fills (dbFill) — ensuring these layers do not render empty