Repository: jupytercad/JupyterCAD (language: python)

Issue:
Dynamic Selection Colors Based on Object Color
Currently, the selection color is static and defaults to blue. While this works, it may not always provide the best visual clarity, especially when working with a variety of object colors. The static blue selection color can sometimes clash with the colors of certain objects, making it difficult to distinguish selections and reducing overall visual appeal.

We suggest making the selection color dynamic, where the selection highlight color is based on the object's original color. 
A slight alteration in brightness or saturation could be used to highlight the selected object without drastically changing its appearance.


Discussion / hints:
Thanks for starting an issue for this! Indeed we should improve the visual indication about selection.

I kind of like the [threejs editor](https://threejs.org/editor/) approach of displaying the bounding box of the selected mesh (see video). Also the fact that position controls are showing up upon selection helps a lot (see the work started in https://github.com/jupytercad/JupyterCAD/pull/151). If we take this approach, I'm not sure dynamic coloring is really needed.

https://github.com/user-attachments/assets/7978fe76-693e-4afb-bbd6-b7f314122115

Thanks @martinRenou, this also makes much sense and I'll try to put together a draft for this bounding box thing and update.

Fix patch:
diff --git a/packages/base/src/3dview/helpers.ts b/packages/base/src/3dview/helpers.ts
index a519a6d7..d1dc4b81 100644
--- a/packages/base/src/3dview/helpers.ts
+++ b/packages/base/src/3dview/helpers.ts
@@ -22,7 +22,8 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast;
 
 export const DEFAULT_MESH_COLOR_CSS = '--jp-inverse-layout-color4';
 export const DEFAULT_EDGE_COLOR_CSS = '--jp-inverse-layout-color2';
-export const SELECTED_MESH_COLOR_CSS = '--jp-brand-color0';
+export const BOUNDING_BOX_COLOR_CSS = '--jp-brand-color0';
+export const SELECTION_BOUNDING_BOX = 'selectionBoundingBox';
 
 export const DEFAULT_MESH_COLOR = new THREE.Color(
   getCSSVariableColor(DEFAULT_MESH_COLOR_CSS)
@@ -30,8 +31,8 @@ export const DEFAULT_MESH_COLOR = new THREE.Color(
 export const DEFAULT_EDGE_COLOR = new THREE.Color(
   getCSSVariableColor(DEFAULT_EDGE_COLOR_CSS)
 );
-export const SELECTED_MESH_COLOR = new THREE.Color(
-  getCSSVariableColor(SELECTED_MESH_COLOR_CSS)
+export const BOUNDING_BOX_COLOR = new THREE.Color(
+  getCSSVariableColor(BOUNDING_BOX_COLOR_CSS)
 );
 
 export type BasicMesh = THREE.Mesh<
@@ -119,8 +120,7 @@ export function buildShape(options: {
   mainMesh: THREE.Mesh<THREE.BufferGeometry, THREE.MeshPhongMaterial>;
   edgesMeshes: LineSegments2[];
 } | null {
-  const { objName, data, isSolid, clippingPlanes, selected, objColor } =
-    options;
+  const { objName, data, isSolid, clippingPlanes, objColor } = options;
   const { faceList, edgeList, jcObject } = data;
 
   const vertices: Array<number> = [];
@@ -221,10 +221,6 @@ export function buildShape(options: {
     type: 'shape'
   };
 
-  if (selected) {
-    mainMesh.material.color = SELECTED_MESH_COLOR;
-  }
-
   let edgeIdx = 0;
   const edgesMeshes: LineSegments2[] = [];
   for (const edge of edgeList) {
diff --git a/packages/base/src/3dview/mainview.tsx b/packages/base/src/3dview/mainview.tsx
index a860a1c4..95121d5a 100644
--- a/packages/base/src/3dview/mainview.tsx
+++ b/packages/base/src/3dview/mainview.tsx
@@ -43,8 +43,9 @@ import {
   IPickedResult,
   IPointer,
   SELECTED_LINEWIDTH,
-  SELECTED_MESH_COLOR,
-  SELECTED_MESH_COLOR_CSS,
+  BOUNDING_BOX_COLOR,
+  BOUNDING_BOX_COLOR_CSS,
+  SELECTION_BOUNDING_BOX,
   buildShape,
   computeExplodedState,
   projectVector
@@ -208,7 +209,7 @@ export class MainView extends React.Component<IProps, IStates> {
     if (this.divRef.current !== null) {
       DEFAULT_MESH_COLOR.set(getCSSVariableColor(DEFAULT_MESH_COLOR_CSS));
       DEFAULT_EDGE_COLOR.set(getCSSVariableColor(DEFAULT_EDGE_COLOR_CSS));
-      SELECTED_MESH_COLOR.set(getCSSVariableColor(SELECTED_MESH_COLOR_CSS));
+      BOUNDING_BOX_COLOR.set(getCSSVariableColor(BOUNDING_BOX_COLOR_CSS));
 
       this._camera = new THREE.PerspectiveCamera(50, 2, 0.1, 1000);
       this._camera.position.set(8, 8, 8);
@@ -555,8 +556,12 @@ export class MainView extends React.Component<IProps, IStates> {
     if (intersects.length > 0) {
       // Find the first intersection with a visible object
       for (const intersect of intersects as ILineIntersection[]) {
-        // Object is hidden
-        if (!intersect.object.visible || !intersect.object.parent?.visible) {
+        // Object is hidden or a bounding box
+        if (
+          !intersect.object.visible ||
+          !intersect.object.parent?.visible ||
+          intersect.object.name === SELECTION_BOUNDING_BOX
+        ) {
           continue;
         }
 
@@ -721,7 +726,7 @@ export class MainView extends React.Component<IProps, IStates> {
 
           if (selectedNames.includes(el.name)) {
             this._selectedMeshes.push(el as any as BasicMesh);
-            el.material.color = SELECTED_MESH_COLOR;
+            el.material.color = originalEdgeColor;
             el.material.linewidth = SELECTED_LINEWIDTH;
             el.userData.originalColor = originalEdgeColor.clone();
           } else {
@@ -963,11 +968,9 @@ export class MainView extends React.Component<IProps, IStates> {
         selectedMesh.material.color = originalColor;
       }
 
-      const material = selectedMesh.material as THREE.Material & {
-        linewidth?: number;
-      };
-      if (material?.linewidth) {
-        material.linewidth = DEFAULT_LINEWIDTH;
+      const boundingBox = selectedMesh.getObjectByName(SELECTION_BOUNDING_BOX);
+      if (boundingBox) {
+        selectedMesh.remove(boundingBox);
       }
     }
 
@@ -982,23 +985,31 @@ export class MainView extends React.Component<IProps, IStates> {
         continue;
       }
 
-      // Prevents object from going back to DEFAULT_MESH_COLOR
-      if (!selectedMesh.userData.originalColor) {
-        selectedMesh.userData.originalColor =
-          selectedMesh.material.color.clone();
-      }
-
       this._selectedMeshes.push(selectedMesh);
-      if (selectedMesh?.material?.color) {
-        selectedMesh.material.color = SELECTED_MESH_COLOR;
-      }
 
-      const material = selectedMesh.material as THREE.Material & {
-        linewidth?: number;
-      };
-      if (material?.linewidth) {
-        material.linewidth = SELECTED_LINEWIDTH;
-      }
+      // Create and add bounding box
+      const geometry = new THREE.BoxGeometry(1, 1, 1);
+      const material = new THREE.LineBasicMaterial({
+        color: BOUNDING_BOX_COLOR,
+        depthTest: false
+      });
+      const boundingBox = new THREE.LineSegments(
+        new THREE.EdgesGeometry(geometry),
+        material
+      );
+      boundingBox.name = SELECTION_BOUNDING_BOX;
+
+      // Set the bounding box size and position
+      const bbox = new THREE.Box3().setFromObject(selectedMesh);
+      const size = new THREE.Vector3();
+      bbox.getSize(size);
+      boundingBox.scale.copy(size);
+
+      const center = new THREE.Vector3();
+      bbox.getCenter(center);
+      boundingBox.position.copy(center);
+
+      selectedMesh.add(boundingBox);
     }
   }
 
@@ -1355,7 +1366,7 @@ export class MainView extends React.Component<IProps, IStates> {
 
     DEFAULT_MESH_COLOR.set(getCSSVariableColor(DEFAULT_MESH_COLOR_CSS));
     DEFAULT_EDGE_COLOR.set(getCSSVariableColor(DEFAULT_EDGE_COLOR_CSS));
-    SELECTED_MESH_COLOR.set(getCSSVariableColor(SELECTED_MESH_COLOR_CSS));
+    BOUNDING_BOX_COLOR.set(getCSSVariableColor(BOUNDING_BOX_COLOR_CSS));
 
     this.setState(old => ({ ...old, lightTheme }));
   };


Test patch:
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/JCAD-New-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/JCAD-New-linux.png
index 8da2018a..a2ce0fc8 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/JCAD-New-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/JCAD-New-linux.png differ