diff --git a/packages/base/src/3dview/mainview.tsx b/packages/base/src/3dview/mainview.tsx index 25c35be2..ee6b4083 100644 --- a/packages/base/src/3dview/mainview.tsx +++ b/packages/base/src/3dview/mainview.tsx @@ -22,6 +22,7 @@ import { TransformControls } from 'three/examples/jsm/controls/TransformControls import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'; import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'; +import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; import { FloatingAnnotation } from '../annotation'; import { getCSSVariableColor, throttle } from '../tools'; @@ -55,6 +56,7 @@ import { SPLITVIEW_BACKGROUND_COLOR_CSS } from './helpers'; import { MainViewModel } from './mainviewmodel'; +import { Measurement } from './measurement'; import { Spinner } from './spinner'; interface IProps { viewModel: MainViewModel; @@ -81,6 +83,7 @@ interface IStates { rotationSnapValue: number; translationSnapValue: number; transformMode: string | undefined; + measurement: boolean; } interface ILineIntersection extends THREE.Intersection { @@ -131,7 +134,8 @@ export class MainView extends React.Component { explodedViewFactor: 0, rotationSnapValue: 10, translationSnapValue: 1, - transformMode: 'translate' + transformMode: 'translate', + measurement: false }; this._model.settingsChanged.connect(this._handleSettingsChange, this); @@ -165,15 +169,25 @@ export class MainView extends React.Component { } componentDidUpdate(oldProps: IProps, oldState: IStates): void { + // Resize the canvas to fit the display area this.resizeCanvasToDisplaySize(); + + // Update transform controls rotation snap if the value has changed if (oldState.rotationSnapValue !== this.state.rotationSnapValue) { this._transformControls.rotationSnap = THREE.MathUtils.degToRad( this.state.rotationSnapValue ); } + + // Update transform controls translation snap if the value has changed if (oldState.translationSnapValue !== this.state.translationSnapValue) { this._transformControls.translationSnap = this.state.translationSnapValue; } + + // Handle measurement display when the measurement tool is toggled. + if (oldState.measurement !== this.state.measurement) { + this._refreshMeasurement(); + } } componentWillUnmount(): void { @@ -317,6 +331,15 @@ export class MainView extends React.Component { this._renderer.setSize(500, 500, false); this._divRef.current.appendChild(this._renderer.domElement); // mount using React ref + // Initialize the CSS2DRenderer for displaying labels + this._labelRenderer = new CSS2DRenderer(); + this._labelRenderer.setSize(500, 500); // Set initial size + this._labelRenderer.domElement.style.position = 'absolute'; + this._labelRenderer.domElement.style.top = '0px'; + // Disable pointer events so the 3D view can be controlled from behind the labels + this._labelRenderer.domElement.style.pointerEvents = 'none'; + this._divRef.current.appendChild(this._labelRenderer.domElement); + this._syncPointer = throttle( (position: THREE.Vector3 | undefined, parent: string | undefined) => { if (position && parent) { @@ -481,6 +504,15 @@ export class MainView extends React.Component { this._transformControls.addEventListener('dragging-changed', event => { this._controls.enabled = !event.value; }); + this._transformControls.addEventListener( + 'change', + throttle(() => { + if (this.state.measurement) { + this._refreshMeasurement(); + // Refresh measurement annotations when the transformed object changes. + } + }, 100) + ); // Update the currently transformed object in the shared model once finished moving this._transformControls.addEventListener('mouseUp', async () => { const updatedObject = this._selectedMeshes[0]; @@ -656,6 +688,7 @@ export class MainView extends React.Component { this._renderer.render(this._scene, this._camera); + this._labelRenderer.render(this._scene, this._camera); // Render the 2D labels on top of the 3D scene this._viewHelper.render(this._renderer); this.updateCameraRotation(); }; @@ -667,6 +700,13 @@ export class MainView extends React.Component { this._divRef.current.clientHeight, false ); + + // Update the size of the label renderer to match the container div. + this._labelRenderer.setSize( + this._divRef.current.clientWidth, + this._divRef.current.clientHeight + ); + if (this._camera instanceof THREE.PerspectiveCamera) { this._camera.aspect = this._divRef.current.clientWidth / this._divRef.current.clientHeight; @@ -1020,6 +1060,7 @@ export class MainView extends React.Component { }); this._updateTransformControls(selectedNames); + this._refreshMeasurement(); // Update the reflength. this._updateRefLength(this._refLength === null); @@ -1358,8 +1399,53 @@ export class MainView extends React.Component { } this._updateTransformControls(selectedNames); + + // Refresh measurement annotations when the selection changes. + this._refreshMeasurement(); } + private _refreshMeasurement = (): void => { + // Clear existing measurement annotations if any. + if (this._measurementGroup) { + this._measurementGroup.clear(); + this._scene.remove(this._measurementGroup); + this._measurementGroup = null; + } + + // If measurement tool is enabled and there are selected meshes, create new measurement annotations. + if (this.state.measurement && this._selectedMeshes.length > 0) { + if (this._selectedMeshes.length === 1) { + // For a single selected object, create an oriented measurement that aligns with the object's rotation. + const mesh = this._selectedMeshes[0]; + const meshGroup = mesh.parent as THREE.Group; + + if (!mesh.geometry.boundingBox) { + mesh.geometry.computeBoundingBox(); + } + const localBox = mesh.geometry.boundingBox!.clone(); + + // Pass the local bounding box, position, and quaternion to the Measurement constructor. + const measurement = new Measurement( + localBox, + meshGroup.position, + meshGroup.quaternion + ); + this._measurementGroup = measurement.group; + this._scene.add(this._measurementGroup); + } else { + // For multiple selected objects, create a single axis-aligned bounding box that encloses all of them. + const combinedBox = new THREE.Box3(); + for (const mesh of this._selectedMeshes) { + const box = new THREE.Box3().setFromObject(mesh.parent!); + combinedBox.union(box); + } + const measurement = new Measurement(combinedBox); + this._measurementGroup = measurement.group; + this._scene.add(this._measurementGroup); + } + } + }; + /* * Attach the transform controls to the current selection, or detach it */ @@ -1674,6 +1760,14 @@ export class MainView extends React.Component { ); } } + if (change.key === 'measurement') { + // Update the measurement state when the measurement tool is toggled. + const measurementEnabled = change.newValue as boolean | undefined; + + if (measurementEnabled !== undefined) { + this.setState(old => ({ ...old, measurement: measurementEnabled })); + } + } } get explodedViewEnabled(): boolean { @@ -2203,6 +2297,7 @@ export class MainView extends React.Component { private _edgeMaterials: any[] = []; private _currentSelection: { [key: string]: ISelection } | null = null; + private _measurementGroup: THREE.Group | null = null; private _scene: THREE.Scene; // Threejs scene private _ambientLight: THREE.AmbientLight; @@ -2210,6 +2305,7 @@ export class MainView extends React.Component { private _cameraLight: THREE.PointLight; private _raycaster = new THREE.Raycaster(); private _renderer: THREE.WebGLRenderer; // Threejs render + private _labelRenderer: CSS2DRenderer; private _requestID: any = null; // ID of window.requestAnimationFrame private _geometry: THREE.BufferGeometry; // Threejs BufferGeometry private _refLength: number | null = null; // Length of bounding box of current object diff --git a/packages/base/src/3dview/measurement.ts b/packages/base/src/3dview/measurement.ts new file mode 100644 index 00000000..7bb0fd94 --- /dev/null +++ b/packages/base/src/3dview/measurement.ts @@ -0,0 +1,167 @@ +/** + * This file defines a class for rendering measurements of a 3D object. + * It uses `three.js` to create dimension lines and labels for a given bounding box. + */ +import * as THREE from 'three'; +import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; + +/** + * A class that displays the dimensions of a THREE.Box3. + * It creates visual annotations (lines and labels) for the X, Y, and Z dimensions. + * The measurement can be axis-aligned or oriented by providing a position and quaternion. + */ +export class Measurement { + private _group: THREE.Group; + private _box: THREE.Box3; + private _quaternion?: THREE.Quaternion; + private _position?: THREE.Vector3; + + /** + * Constructor for the Measurement class. + * @param box The bounding box to measure. + * @param position Optional position to apply to the measurement group for oriented measurements. + * @param quaternion Optional quaternion to apply to the measurement group for oriented measurements. + */ + constructor( + box: THREE.Box3, + position?: THREE.Vector3, + quaternion?: THREE.Quaternion + ) { + this._box = box; + this._position = position; + this._quaternion = quaternion; + this._group = new THREE.Group(); + this.createAnnotations(); + } + + /** + * Removes all annotations from the scene. + */ + clearAnnotations() { + this._group.clear(); + } + + /** + * Creates the dimension lines and labels for the bounding box. + */ + createAnnotations() { + if (!this._box) { + return; + } + + const size = new THREE.Vector3(); + this._box.getSize(size); + + const min = this._box.min; + const max = this._box.max; + + // Create dimension lines only for dimensions with a size greater than a small epsilon. + // This is useful for hiding zero-dimension measurements for 2D objects like edges. + if (size.x > 1e-6) { + this.createDimensionLine( + new THREE.Vector3(min.x, min.y, min.z), + new THREE.Vector3(max.x, min.y, min.z), + 'X', + size.x + ); + } + if (size.y > 1e-6) { + this.createDimensionLine( + new THREE.Vector3(max.x, min.y, min.z), + new THREE.Vector3(max.x, max.y, min.z), + 'Y', + size.y + ); + } + if (size.z > 1e-6) { + this.createDimensionLine( + new THREE.Vector3(max.x, max.y, min.z), + new THREE.Vector3(max.x, max.y, max.z), + 'Z', + size.z + ); + } + + // The annotations are created for an axis-aligned box at the origin, so transform + // the group to match the object's actual position and orientation (if provided). + if (this._quaternion) { + this._group.quaternion.copy(this._quaternion); + } + if (this._position) { + this._group.position.copy(this._position); + } + } + + /** + * Creates a single dimension line with a label. + * @param start The start point of the line. + * @param end The end point of the line. + * @param axis The axis name ('X', 'Y', or 'Z'). + * @param value The length of the dimension. + */ + createDimensionLine( + start: THREE.Vector3, + end: THREE.Vector3, + axis: string, + value: number + ) { + const geometry = new THREE.BufferGeometry().setFromPoints([start, end]); + + // Create a solid white line (to go behind the dashed line to improve + // contrast when measurements pass over objects) + const whiteLineMaterial = new THREE.LineBasicMaterial({ + color: 0xffffff, + linewidth: 4, + depthTest: false, + depthWrite: false, + transparent: true, + opacity: 0.85 + }); + const whiteLine = new THREE.Line(geometry, whiteLineMaterial); + whiteLine.renderOrder = 0; // Ensure white line renders just before the dashed line + this._group.add(whiteLine); + + // Create the dashed line + const dashLineMaterial = new THREE.LineDashedMaterial({ + color: 0x000000, + linewidth: 1, + scale: 1, + dashSize: 0.1, + gapSize: 0.1, + depthTest: false, // Render lines on top of other objects for better visibility + depthWrite: false, + transparent: true + }); + const dashLine = new THREE.Line(geometry.clone(), dashLineMaterial); + dashLine.computeLineDistances(); + dashLine.renderOrder = 1; // Ensure dashed line renders on top of the solid white line + this._group.add(dashLine); + + // Create the label + const labelDiv = document.createElement('div'); + labelDiv.className = 'measurement-label'; + labelDiv.textContent = `${axis}: ${value.toFixed(2)}`; + labelDiv.style.color = 'black'; + labelDiv.style.fontSize = '12px'; + labelDiv.style.backgroundColor = 'rgba(255, 255, 255, 0.7)'; + labelDiv.style.padding = '2px 5px'; + labelDiv.style.borderRadius = '3px'; + const label = new CSS2DObject(labelDiv); + + // Position the label at the midpoint of the line + const midPoint = new THREE.Vector3() + .addVectors(start, end) + .multiplyScalar(0.5); + label.position.copy(midPoint); + + this._group.add(label); + } + + /** + * Getter for the THREE.Group containing the measurement annotations. + * This group can be added to a THREE.Scene to be rendered. + */ + public get group(): THREE.Group { + return this._group; + } +} diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index dfe73d64..c6b7a563 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -28,6 +28,7 @@ import { boxIcon, chamferIcon, clippingIcon, + rulerIcon, coneIcon, cutIcon, cylinderIcon, @@ -784,6 +785,26 @@ export function addCommands( } }); + commands.addCommand(CommandIDs.toggleMeasurement, { + label: trans.__('Toggle Measurement'), + isEnabled: () => { + return tracker.currentWidget !== null; + }, + isToggled: () => { + const current = tracker.currentWidget?.content; + return current?.measurement ?? false; + }, + icon: rulerIcon, + execute: async () => { + const current = tracker.currentWidget?.content; + if (!current) { + return; + } + current.measurement = !current.measurement; + commands.notifyCommandChanged(CommandIDs.toggleMeasurement); + } + }); + tracker.currentChanged.connect(() => { commands.notifyCommandChanged(CommandIDs.updateClipView); }); @@ -960,6 +981,7 @@ export namespace CommandIDs { export const updateExplodedView = 'jupytercad:updateExplodedView'; export const updateCameraSettings = 'jupytercad:updateCameraSettings'; export const updateClipView = 'jupytercad:updateClipView'; + export const toggleMeasurement = 'jupytercad:toggleMeasurement'; export const splitScreen = 'jupytercad:splitScreen'; export const exportJcad = 'jupytercad:exportJcad'; diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 1aa75f8c..2f311ace 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -242,6 +242,14 @@ export class ToolbarWidget extends ReactiveToolbar { commands: options.commands }) ); + this.addItem( + 'Toggle Measurement', + new CommandToolbarButton({ + id: CommandIDs.toggleMeasurement, + label: '', + commands: options.commands + }) + ); this.addItem('separator6', new Separator()); this.addItem( diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index b974b187..ab6f6407 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -20,6 +20,7 @@ import sphereIconStr from '../style/icon/sphere.svg'; import torusIconStr from '../style/icon/torus.svg'; import unionIconStr from '../style/icon/union.svg'; import clippingIconStr from '../style/icon/clipPlane.svg'; +import rulerIconStr from '../style/icon/ruler.svg'; import chamferIconStr from '../style/icon/chamfer.svg'; import filletIconStr from '../style/icon/fillet.svg'; import wireframeIconStr from '../style/icon/wireframe.svg'; @@ -126,6 +127,11 @@ export const clippingIcon = new LabIcon({ svgstr: clippingIconStr }); +export const rulerIcon = new LabIcon({ + name: 'jupytercad:ruler-icon', + svgstr: rulerIconStr +}); + export const chamferIcon = new LabIcon({ name: 'jupytercad:chamfer-icon', svgstr: chamferIconStr diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index d00c1c4a..e6ea115c 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -138,6 +138,7 @@ export class JupyterCadPanel extends SplitPanel { this._view.set('cameraSettings', cameraSettings); this._view.set('explodedView', explodedView); this._view.set('axes', axes); + this._view.set('measurement', false); this._mainViewModel = new MainViewModel({ jcadModel: options.model, @@ -250,6 +251,14 @@ export class JupyterCadPanel extends SplitPanel { this._view.set('transform', value); } + get measurement(): boolean { + return this._view.get('measurement') as boolean; + } + + set measurement(value: boolean) { + this._view.set('measurement', value); + } + get consoleOpened(): boolean { return this._consoleOpened; } diff --git a/packages/base/style/icon/ruler.svg b/packages/base/style/icon/ruler.svg new file mode 100644 index 00000000..da02af4d --- /dev/null +++ b/packages/base/style/icon/ruler.svg @@ -0,0 +1,11 @@ + diff --git a/ui-tests/tests/measurement.spec.ts b/ui-tests/tests/measurement.spec.ts new file mode 100644 index 00000000..228b9e89 --- /dev/null +++ b/ui-tests/tests/measurement.spec.ts @@ -0,0 +1,86 @@ +import { expect, test, galata } from '@jupyterlab/galata'; +import path from 'path'; + +test.use({ autoGoto: false }); + +test.describe('Measurement Test', () => { + test.beforeAll(async ({ request }) => { + const content = galata.newContentsHelper(request); + await content.deleteDirectory('/examples'); + await content.uploadDirectory( + path.resolve(__dirname, '../../examples'), + '/examples' + ); + }); + + let errors = 0; + /** + * Sets up the test environment before each test. + * - Sets the viewport size. + * - Listens for console errors and increments the error count. + */ + test.beforeEach(async ({ page }) => { + page.setViewportSize({ width: 1920, height: 1080 }); + page.on('console', message => { + if (message.type() === 'error') { + console.log('ERROR MSG', message.text()); + errors += 1; + } + }); + }); + + /** + * Resets the error count after each test. + */ + test.afterEach(async () => { + errors = 0; + }); + + test('Toggle measurement displays annotations', async ({ page }) => { + await page.goto(); + + const fullPath = 'examples/test.jcad'; + await page.notebook.openByPath(fullPath); + await page.notebook.activate(fullPath); + await page.locator('div.jpcad-Spinner').waitFor({ state: 'hidden' }); + + // Ensure left sidebar (object tree) is open and right sidebar is closed + await page.sidebar.open('left'); + await page.sidebar.close('right'); + await page.waitForTimeout(500); + + // Select 'box2' in the tree so measurement has a selection to measure + await page + .locator('[data-test-id="react-tree-root"]') + .getByText('box2') + .click(); + + // Make sure any modal is dismissed + if (await page.getByRole('button', { name: 'Ok' }).isVisible()) { + await page.getByRole('button', { name: 'Ok' }).click(); + } + + // Enable measurement after selection + await page.getByTitle('Toggle Measurement').click(); + + // Wait for measurement labels to appear in the label renderer + const label = page.locator('.measurement-label'); + await label.first().waitFor({ state: 'visible' }); + + expect(errors).toBe(0); + + const main = await page.$('#jp-main-split-panel'); + if (main) { + expect(await main.screenshot()).toMatchSnapshot({ + name: `Measurement-On-test.jcad.png`, + maxDiffPixelRatio: 0.01 + }); + } + + // Disable measurement and ensure labels disappear + await page.getByTitle('Toggle Measurement').click(); + await expect(page.locator('.measurement-label')).toHaveCount(0, { + timeout: 1000 + }); + }); +}); diff --git a/ui-tests/tests/measurement.spec.ts-snapshots/Measurement-On-test-jcad-chromium-linux.png b/ui-tests/tests/measurement.spec.ts-snapshots/Measurement-On-test-jcad-chromium-linux.png new file mode 100644 index 00000000..2835a0c7 Binary files /dev/null and b/ui-tests/tests/measurement.spec.ts-snapshots/Measurement-On-test-jcad-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png index 8aec558d..2411c56a 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-0-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png index 036baff4..a4d94c24 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-1-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-chromium-linux.png index aa7398ef..a5ecc164 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-2-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png index 12948313..85168179 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/dark-Notebook-ipynb-cell-3-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-chromium-linux.png index 8852e210..1ecae400 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-0-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-chromium-linux.png index ace9055b..3aab07af 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-2-chromium-linux.png differ diff --git a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-chromium-linux.png b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-chromium-linux.png index 069b35e3..58a52f02 100644 Binary files a/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-chromium-linux.png and b/ui-tests/tests/notebook.spec.ts-snapshots/light-Notebook-ipynb-cell-3-chromium-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-chromium-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-chromium-linux.png index 569e6135..b90d74ad 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-chromium-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-chromium-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-chromium-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-chromium-linux.png index 95c12ad7..a48969cd 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-chromium-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-chromium-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-chromium-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-chromium-linux.png index b8a76c5f..947fac8d 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-chromium-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-chromium-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-chromium-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-chromium-linux.png index 34f6cf9e..c13e2d08 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-chromium-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-chromium-linux.png differ diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Remove-test-jcad-chromium-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Remove-test-jcad-chromium-linux.png index ffdda3a2..1c5afab6 100644 Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Remove-test-jcad-chromium-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Remove-test-jcad-chromium-linux.png differ