Repository: jupytercad/JupyterCAD (language: python)

Issue:
Transform Controls not disabled by default
Transform controls shall be disabled until they are enabled by "Toggle Transform Controls" Button, but at the moment they are not disabled even though they are not enabled via button.


Discussion / hints:

Fix patch:
diff --git a/packages/base/src/3dview/helpers.ts b/packages/base/src/3dview/helpers.ts
index 393cdc3e..11330efd 100644
--- a/packages/base/src/3dview/helpers.ts
+++ b/packages/base/src/3dview/helpers.ts
@@ -8,6 +8,7 @@ import {
 import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
 import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
 import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
+import { IJCadObject } from '@jupytercad/schema';
 
 import { getCSSVariableColor } from '../tools';
 
@@ -64,6 +65,12 @@ export interface IMouseDrag {
   end: THREE.Vector2;
 }
 
+export interface IMeshGroupMetadata {
+  type: string;
+  jcObject: IJCadObject;
+  [key: string]: any;
+}
+
 export function projectVector(options: {
   vector: THREE.Vector3;
   camera: THREE.Camera;
@@ -81,6 +88,26 @@ export function projectVector(options: {
   );
 }
 
+export function getQuaternion(jcObject: IJCadObject): THREE.Quaternion {
+  const placement = jcObject?.parameters?.Placement;
+
+  const angle = placement.Angle;
+  const axis = placement.Axis;
+  const axisVector = new THREE.Vector3(axis[0], axis[1], axis[2]);
+  axisVector.normalize();
+
+  const angleRad = (angle * Math.PI) / 180;
+  const halfAngle = angleRad / 2;
+  const sinHalfAngle = Math.sin(halfAngle);
+
+  return new THREE.Quaternion(
+    axisVector.x * sinHalfAngle,
+    axisVector.y * sinHalfAngle,
+    axisVector.z * sinHalfAngle,
+    Math.cos(halfAngle)
+  ).normalize();
+}
+
 export function computeExplodedState(options: {
   mesh: BasicMesh;
   boundingGroup: THREE.Box3;
@@ -88,18 +115,29 @@ export function computeExplodedState(options: {
 }) {
   const { mesh, boundingGroup, factor } = options;
   const center = new THREE.Vector3();
+  const meshGroup = mesh.parent as THREE.Object3D;
   boundingGroup.getCenter(center);
 
   const oldGeometryCenter = new THREE.Vector3();
   mesh.geometry.boundingBox?.getCenter(oldGeometryCenter);
 
+  const meshGroupQuaternion = getQuaternion(meshGroup.userData.jcObject);
+  const meshGroupPositionArray =
+    meshGroup.userData.jcObject.parameters?.Placement.Position;
+  const meshGroupPosition = new THREE.Vector3(
+    meshGroupPositionArray[0],
+    meshGroupPositionArray[1],
+    meshGroupPositionArray[2]
+  );
+  oldGeometryCenter.applyQuaternion(meshGroupQuaternion).add(meshGroupPosition);
+
   const centerToMesh = new THREE.Vector3(
     oldGeometryCenter.x - center.x,
     oldGeometryCenter.y - center.y,
     oldGeometryCenter.z - center.z
   );
-  const distance = centerToMesh.length() * factor;
 
+  const distance = centerToMesh.length() * factor;
   centerToMesh.normalize();
 
   const newGeometryCenter = new THREE.Vector3(
@@ -112,7 +150,7 @@ export function computeExplodedState(options: {
     oldGeometryCenter,
     newGeometryCenter,
     vector: centerToMesh,
-    distance
+    distance: distance
   };
 }
 
@@ -134,18 +172,33 @@ export function buildShape(options: {
 
   const vertices: Array<number> = [];
   const triangles: Array<number> = [];
+  const placement = data?.jcObject?.parameters?.Placement;
+  const objPosition = placement.Position;
+
+  const objQuaternion = getQuaternion(jcObject);
+  const inverseQuaternion = objQuaternion.clone().invert();
 
   let vInd = 0;
   if (faceList.length === 0 && edgeList.length === 0) {
     return null;
   }
   for (const face of faceList) {
-    // Copy Vertices into three.js Vector3 List
     const vertexCoorLength = face.vertexCoord.length;
-    for (let ii = 0; ii < vertexCoorLength; ii++) {
-      vertices.push(face.vertexCoord[ii]);
+    for (let ii = 0; ii < vertexCoorLength; ii += 3) {
+      const vertex = new THREE.Vector3(
+        face.vertexCoord[ii],
+        face.vertexCoord[ii + 1],
+        face.vertexCoord[ii + 2]
+      );
+      // Undo placement from the vertices, we want the placement done on the THREE.Object3D (Mesh), not the geometry
+      vertex.sub(
+        new THREE.Vector3(objPosition[0], objPosition[1], objPosition[2])
+      );
+      vertex.applyQuaternion(inverseQuaternion);
+
+      vertices.push(vertex.x, vertex.y, vertex.z);
     }
-    // Sort Triangles into a three.js Face List
+
     const triIndexesLength = face.triIndexes.length;
     for (let i = 0; i < triIndexesLength; i += 3) {
       triangles.push(
@@ -246,8 +299,25 @@ export function buildShape(options: {
       polygonOffsetFactor: -5,
       polygonOffsetUnits: -5
     });
+
+    const transformedVertices: number[] = [];
+    for (let i = 0; i < edge.vertexCoord.length; i += 3) {
+      const vertex = new THREE.Vector3(
+        edge.vertexCoord[i],
+        edge.vertexCoord[i + 1],
+        edge.vertexCoord[i + 2]
+      );
+      // Undo placement from the vertices, we want the placement done on the THREE.Object3D (Mesh), not the geometry
+      vertex.sub(
+        new THREE.Vector3(objPosition[0], objPosition[1], objPosition[2])
+      );
+      vertex.applyQuaternion(inverseQuaternion);
+
+      transformedVertices.push(vertex.x, vertex.y, vertex.z);
+    }
+
     const edgeGeometry = new LineGeometry();
-    edgeGeometry.setPositions(edge.vertexCoord);
+    edgeGeometry.setPositions(transformedVertices);
     const edgesMesh = new LineSegments2(edgeGeometry, edgeMaterial);
     edgesMesh.name = `edge-${objName}-${edgeIdx}`;
     edgesMesh.userData = {
@@ -261,7 +331,7 @@ export function buildShape(options: {
     edgeIdx++;
   }
 
-  const bbox = new THREE.Box3().setFromObject(mainMesh);
+  const bbox = new THREE.Box3().setFromObject(meshGroup);
   const size = new THREE.Vector3();
   bbox.getSize(size);
   const center = new THREE.Vector3();
@@ -275,8 +345,14 @@ export function buildShape(options: {
   boundingBox.visible = false;
   boundingBox.name = SELECTION_BOUNDING_BOX;
   meshGroup.add(boundingBox);
+  meshGroup.userData.type = 'shape';
 
   meshGroup.add(mainMesh);
 
+  meshGroup.applyQuaternion(objQuaternion);
+  meshGroup.position.copy(
+    new THREE.Vector3(objPosition[0], objPosition[1], objPosition[2])
+  );
+
   return { meshGroup, mainMesh, edgesMeshes };
 }
diff --git a/packages/base/src/3dview/mainview.tsx b/packages/base/src/3dview/mainview.tsx
index 74d1a12e..8e9a38a4 100644
--- a/packages/base/src/3dview/mainview.tsx
+++ b/packages/base/src/3dview/mainview.tsx
@@ -49,7 +49,8 @@ import {
   buildShape,
   computeExplodedState,
   projectVector,
-  IMouseDrag
+  IMouseDrag,
+  IMeshGroupMetadata
 } from './helpers';
 import { MainViewModel } from './mainviewmodel';
 import { Spinner } from './spinner';
@@ -426,11 +427,10 @@ export class MainView extends React.Component<IProps, IStates> {
         const obj = this._model.sharedModel.getObjectByName(objectName);
 
         if (obj && obj.parameters && obj.parameters.Placement) {
-          const positionArray = obj?.parameters?.Placement?.Position;
           const newPosition = [
-            positionArray[0] + updatedPosition.x,
-            positionArray[1] + updatedPosition.y,
-            positionArray[2] + updatedPosition.z
+            updatedPosition.x,
+            updatedPosition.y,
+            updatedPosition.z
           ];
 
           this._mainViewModel.maybeUpdateObjectParameters(objectName, {
@@ -444,6 +444,7 @@ export class MainView extends React.Component<IProps, IStates> {
       });
       this._scene.add(this._transformControls);
       this._transformControls.setMode('translate');
+      this._transformControls.setSpace('local');
       this._transformControls.enabled = false;
       this._transformControls.visible = false;
 
@@ -782,7 +783,7 @@ export class MainView extends React.Component<IProps, IStates> {
       if (output) {
         const { meshGroup, mainMesh, edgesMeshes } = output;
         if (meshGroup.userData.jcObject.visible) {
-          this._boundingGroup.expandByObject(mainMesh);
+          this._boundingGroup.expandByObject(meshGroup);
         }
 
         // Save original color for the main mesh
@@ -1202,18 +1203,6 @@ export class MainView extends React.Component<IProps, IStates> {
       if (matchingChild) {
         this._transformControls.attach(matchingChild as BasicMesh);
 
-        const obj = this._model.sharedModel.getObjectByName(selectedMeshName);
-        const positionArray = obj?.parameters?.Placement?.Position;
-
-        if (positionArray && positionArray.length === 3) {
-          const positionVector = new THREE.Vector3(
-            positionArray[0],
-            positionArray[1],
-            positionArray[2]
-          );
-          this._transformControls.position.copy(positionVector);
-        }
-
         this._transformControls.visible = this.state.transform;
         this._transformControls.enabled = this.state.transform;
 
@@ -1506,6 +1495,9 @@ export class MainView extends React.Component<IProps, IStates> {
       this._explodedViewLinesHelperGroup = new THREE.Group();
 
       for (const group of this._meshGroup?.children as THREE.Group[]) {
+        const groupMetadata = group.userData as IMeshGroupMetadata;
+        const positionArray =
+          groupMetadata.jcObject.parameters?.Placement.Position;
         const explodedState = computeExplodedState({
           mesh: group.getObjectByName(
             group.name.replace('-group', '')
@@ -1514,8 +1506,13 @@ export class MainView extends React.Component<IProps, IStates> {
           factor: this._explodedView.factor
         });
 
-        group.position.set(0, 0, 0);
-        group.translateOnAxis(explodedState.vector, explodedState.distance);
+        group.position.copy(
+          new THREE.Vector3(
+            positionArray[0] + explodedState.vector.x * explodedState.distance,
+            positionArray[1] + explodedState.vector.y * explodedState.distance,
+            positionArray[2] + explodedState.vector.z * explodedState.distance
+          )
+        );
 
         // Draw lines
         const material = new THREE.LineBasicMaterial({
@@ -1535,10 +1532,20 @@ export class MainView extends React.Component<IProps, IStates> {
 
       this._scene.add(this._explodedViewLinesHelperGroup);
     } else {
-      // Exploded view is disabled, we reset the initial positions
-      for (const mesh of this._meshGroup?.children as BasicMesh[]) {
-        mesh.position.set(0, 0, 0);
+      // Reset objects to their original positions
+      for (const group of this._meshGroup?.children as THREE.Group[]) {
+        const groupMetadata = group.userData as IMeshGroupMetadata;
+        const positionArray =
+          groupMetadata.jcObject.parameters?.Placement.Position;
+        group.position.copy(
+          new THREE.Vector3(
+            positionArray[0],
+            positionArray[1],
+            positionArray[2]
+          )
+        );
       }
+
       this._explodedViewLinesHelperGroup?.removeFromParent();
     }
   }


Test patch:
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-linux.png
index ed30e581..f711d6a4 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Exploded-test-jcad-linux.png differ
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-linux.png
index 3a0dfbb7..698ad422 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-Cut-test-jcad-linux.png differ
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-linux.png
index 8bbc3336..8db27aa8 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/MultiSelect-test-jcad-linux.png differ
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-linux.png
index cc16267d..e53af66b 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Operator-Edit-test-jcad-linux.png differ
diff --git a/ui-tests/tests/ui.spec.ts-snapshots/Render-test-jcad-linux.png b/ui-tests/tests/ui.spec.ts-snapshots/Render-test-jcad-linux.png
index a7b608ed..5e56665e 100644
Binary files a/ui-tests/tests/ui.spec.ts-snapshots/Render-test-jcad-linux.png and b/ui-tests/tests/ui.spec.ts-snapshots/Render-test-jcad-linux.png differ