Repository: jupytercad/JupyterCAD (language: python)

Issue:
Add `ViewHelper` to JupyterCAD Editor
This issue proposes the addition of a `ViewHelper` to the JupyterCAD editor. This would greatly enhance user experience by providing a 3D viewport gizmo that allows users to interactively control and view camera orientations.

The `ViewHelper` can offer users a visual and interactive tool to change views, similar to the feature demonstrated in this repository: [three-viewport-gizmo](https://github.com/Fennec-hub/three-viewport-gizmo). A working example can be found here: [three-viewport-gizmo example](https://fennec-hub.github.io/three-viewport-gizmo/).

### Expected Behavior

- A small interactive sphere-like gizmo should appear in the top or bottom-right corner of the JupyterCAD canvas.
- Users should be able to click on faces, edges, and corners of the gizmo to change the view orientation (top, bottom, left, right, front, back).
- The `ViewHelper` should remain fixed on the screen and should not be affected by camera movements such as panning, zooming, or rotating the 3D model in the main scene.
- It should provide smooth transitions between camera views, enhancing navigation in complex 3D scenes.

### Proposed Implementation

- The `three-viewport-gizmo` library can be utilized to integrate the `ViewHelper` functionality within the JupyterCAD editor.
- The gizmo should be rendered separately from the main 3D scene to avoid interference with the model itself, and it should be responsive to user interactions (clicks and pointer movements).
- See the example on how this can be implemented: [three-viewport-gizmo example](https://fennec-hub.github.io/three-viewport-gizmo/).


Discussion / hints:
Thanks! This extension looks really good.

💯 on adding it to JupyterCAD

Fix patch:
diff --git a/packages/base/src/3dview/mainview.tsx b/packages/base/src/3dview/mainview.tsx
index 5e631007..1f299612 100644
--- a/packages/base/src/3dview/mainview.tsx
+++ b/packages/base/src/3dview/mainview.tsx
@@ -22,6 +22,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
 import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
 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 { FloatingAnnotation } from '../annotation';
 import { getCSSVariableColor, isLightTheme, throttle } from '../tools';
@@ -221,7 +222,10 @@ export class MainView extends React.Component<IProps, IStates> {
         antialias: true,
         stencil: true
       });
+
+      this._clock = new THREE.Clock();
       // this._renderer.setPixelRatio(window.devicePixelRatio);
+      this._renderer.autoClear = false;
       this._renderer.setClearColor(0x000000, 0);
       this._renderer.setSize(500, 500, false);
       this.divRef.current.appendChild(this._renderer.domElement); // mount using React ref
@@ -351,11 +355,33 @@ export class MainView extends React.Component<IProps, IStates> {
 
       this._transformControls.enabled = false;
       this._transformControls.visible = false;
+
+      // ViewHelper setup
+      this._viewHelper = new ViewHelper(
+        this._camera,
+        this._renderer.domElement
+      );
+      this._viewHelper.center = this._controls.target;
+
+      const viewHelperDiv = document.createElement('div');
+      viewHelperDiv.id = 'viewHelper';
+      viewHelperDiv.style.position = 'absolute';
+      viewHelperDiv.style.right = '0px';
+      viewHelperDiv.style.bottom = '0px';
+      viewHelperDiv.style.height = '128px';
+      viewHelperDiv.style.width = '128px';
+
+      this.divRef.current.appendChild(viewHelperDiv);
+
+      viewHelperDiv.addEventListener('pointerup', event =>
+        this._viewHelper.handleClick(event)
+      );
     }
   };
 
   animate = (): void => {
     this._requestID = window.requestAnimationFrame(this.animate);
+    const delta = this._clock.getDelta();
 
     for (const material of this._edgeMaterials) {
       material.resolution.set(
@@ -372,11 +398,15 @@ export class MainView extends React.Component<IProps, IStates> {
         this._clippingPlaneMesh.position.z - this._clippingPlane.normal.z
       );
     }
+    if (this._viewHelper.animating) {
+      this._viewHelper.update(delta);
+    }
 
     this._controls.update();
     this._renderer.setRenderTarget(null);
-    this._renderer.clearDepth();
+    this._renderer.clear();
     this._renderer.render(this._scene, this._camera);
+    this._viewHelper.render(this._renderer);
   };
 
   resizeCanvasToDisplaySize = (): void => {
@@ -1411,6 +1441,8 @@ export class MainView extends React.Component<IProps, IStates> {
   private _controls: OrbitControls; // Mouse controls
   private _transformControls: TransformControls; // Mesh position/rotation controls
   private _pointer3D: IPointer | null = null;
+  private _clock: THREE.Clock;
+  private _viewHelper: ViewHelper;
   private _collaboratorPointers: IDict<IPointer>;
   private _pointerGeometry: THREE.SphereGeometry;
   private _contextMenu: ContextMenu;


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 883015e5..6c2b9b72 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
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/JCAD-Undo-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/JCAD-Undo-linux.png
index 1df4f085..8cab4f30 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/JCAD-Undo-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/JCAD-Undo-linux.png differ