Skip to content

Commit

Permalink
UI: make mesh grid drawing more performant
Browse files Browse the repository at this point in the history
Replace per-cell result ellipse fabric objects with one single
custom fabric object. When the cells are handful pixels large,
switch to drawing the results as a rectangle. It does not make a
visual difference, but is faster.

These changes reduces UI latency to usable levels, when drawing
meshes with 100k+ cells in 'RGB' mode.

Don't draw the dashed inner lines, when cells are small, to reduce
visual clutter.
  • Loading branch information
elmjag authored and marcus-oscarsson committed Sep 26, 2024
1 parent d8748c0 commit 87aa694
Showing 1 changed file with 160 additions and 56 deletions.
216 changes: 160 additions & 56 deletions ui/src/components/SampleView/DrawGridPlugin.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import 'fabric';

const TAU = Math.PI * 2;

const { fabric } = window;

/**
* Based on cell width and height in pixels,
* decide if we should do drawing in 'small cell size' mode.
*
* returns true if 'small cell size' mode should be used, false otherwise
*/
function inSmallCellSizeMode(cellWidth, cellHeight) {
const MIN_CELL_PIXELS_SIZE = 6;
return cellWidth < MIN_CELL_PIXELS_SIZE || cellHeight < MIN_CELL_PIXELS_SIZE;
}

/**
* Fabric Shape for drawing grid (defined by GridData)
*/
Expand All @@ -14,6 +27,79 @@ const GridGroup = fabric.util.createClass(fabric.Group, {
},
});

/**
* Custom Fabric shape, for displaying mesh scan results.
*
* This is an optimization for drawing mesh grids with large number of
* cells. Using one single custom fabric object for drawing results for
* all cells is faster, then having a per cell fabric object.
*
* Another optimization is to switch to drawing rectangles instead of ellipses,
* when cells are small. When cell's are a handful of pixels large, there is no
* noticeable visual difference between a rectangle and an ellipse. However, it
* appears that drawing rectangles is much faster.
*
*/
const MeshGridResult = fabric.util.createClass(fabric.Object, {
type: 'GridGroup',

initialize(objects, options = {}) {
this.callSuper('initialize', objects, options);
},

_render(ctx) {
if (inSmallCellSizeMode(this.cellTW, this.cellTH)) {
//
// the cells are small, draw them as rectangles
//

const xOffset = this.cellHSpace / 2;
const yOffset = this.cellVSpace / 2;
const width = this.cellTW - this.cellHSpace;
const height = this.cellTH - this.cellVSpace;

for (let y = 0; y < this.cellRows; y += 1) {
for (let x = 0; x < this.cellColumns; x += 1) {
ctx.beginPath();
ctx.fillStyle = this.fillingMatrix[x][y];
ctx.fillRect(
xOffset + x * this.cellTW,
yOffset + y * this.cellTH,
width,
height,
);
}
}
} else {
//
// normal cells, draw them as ellipses
//

const xOffset = this.cellTW / 2;
const yOffset = this.cellTH / 2;
const width = (this.cellTW - this.cellHSpace) / 2;
const height = (this.cellTH - this.cellVSpace) / 2;

for (let y = 0; y < this.cellRows; y += 1) {
for (let x = 0; x < this.cellColumns; x += 1) {
ctx.beginPath();
ctx.fillStyle = this.fillingMatrix[x][y];
ctx.ellipse(
xOffset + x * this.cellTW,
yOffset + y * this.cellTH,
width,
height,
0,
0,
TAU,
);
ctx.fill();
}
}
}
},
});

/**
* GridData object defines a grid
*
Expand Down Expand Up @@ -342,6 +428,55 @@ export default class DrawGridPlugin {
return fillingMatrix;
}

/**
* Adds dashed inner lines to a mesh grid.
*
* Does nothing if mesh is not selected,
* or if we are drawing in small cell size' mode.
*/
addInnerLines(shapes, gridData, left, top, height, width, cellTH, cellTW) {
if (!gridData.selected) {
/* we only draw inner lines for selected meshes */
return;
}

if (inSmallCellSizeMode(cellTW, cellTH)) {
/* we don't draw inner lines for small cells, to reduce visual clutter */
return;
}

const lineColor = 'rgba(136, 255, 91, 0.5)';
const strokeArray = [5, 5];

for (let nw = 1; nw < gridData.numCols; nw++) {
shapes.push(
new fabric.Line(
[left + cellTW * nw, top, left + cellTW * nw, top + height],
{
stroke: lineColor,
strokeDashArray: strokeArray,
hasControls: false,
selectable: false,
},
),
);
}

for (let nh = 1; nh < gridData.numRows; nh++) {
shapes.push(
new fabric.Line(
[left, top + cellTH * nh, left + width, top + cellTH * nh],
{
stroke: lineColor,
strokeDashArray: strokeArray,
hasControls: false,
selectable: false,
},
),
);
}
}

/**
* Creates a Fabric GridGroup shape from a GridData object
*
Expand Down Expand Up @@ -376,40 +511,19 @@ export default class DrawGridPlugin {
? 'rgba(136, 255, 91, 1)'
: 'rgba(228, 255, 9, 0.5)';
color = gridData.result?.length > 0 ? 'rgba(228, 255, 9, 1)' : color;
const lineColor = gridData.selected
? 'rgba(136, 255, 91, 0.5)'
: 'rgba(228, 255, 9, 0)';
const outlineStrokeArray = gridData.selected ? [] : [5, 5];
const innerStrokeArray = gridData.selected ? [5, 5] : [0, 0];

if (cellTW > 0 && cellTH > 0) {
for (let nw = 1; nw < gridData.numCols; nw++) {
shapes.push(
new fabric.Line(
[left + cellTW * nw, top, left + cellTW * nw, top + height],
{
stroke: lineColor,
strokeDashArray: innerStrokeArray,
hasControls: false,
selectable: false,
},
),
);
}

for (let nh = 1; nh < gridData.numRows; nh++) {
shapes.push(
new fabric.Line(
[left, top + cellTH * nh, left + width, top + cellTH * nh],
{
stroke: lineColor,
strokeDashArray: innerStrokeArray,
hasControls: false,
selectable: false,
},
),
);
}
this.addInnerLines(
shapes,
gridData,
left,
top,
height,
width,
cellTH,
cellTW,
);

if (!this.drawing) {
if (this.gridResultFormat === 'RGB') {
Expand All @@ -419,6 +533,21 @@ export default class DrawGridPlugin {
gridData.numRows,
);

shapes.push(
new MeshGridResult({
left,
top,
cellColumns: gridData.numCols,
cellRows: gridData.numRows,
gridData,
cellTW,
cellTH,
cellHSpace,
cellVSpace,
fillingMatrix,
}),
);

for (let nw = 0; nw < gridData.numCols; nw++) {
for (let nh = 0; nh < gridData.numRows; nh++) {
const cellCount = this.countCells(
Expand All @@ -429,31 +558,6 @@ export default class DrawGridPlugin {
gridData.numCols,
);

shapes.push(
new fabric.Ellipse({
left: left + cellHSpace / 2 + cellTW * nw,
top: top + cellVSpace / 2 + cellTH * nh,
width: cellWidth,
height: cellHeight,
fill: fillingMatrix[nw][nh],
stroke: 'rgba(0,0,0,0)',
hasControls: false,
selectable: false,
hasRotatingPoint: false,
lockMovementX: true,
lockMovementY: true,
lockScalingX: true,
lockScalingY: true,
lockRotation: true,
hoverCursor: 'pointer',
originX: 'left',
originY: 'top',
rx: cellWidth / 2,
ry: cellHeight / 2,
cell: cellCount,
}),
);

if (this.include_cell_labels) {
shapes.push(
new fabric.Text(cellCount, {
Expand Down

0 comments on commit 87aa694

Please sign in to comment.