@@ -86,6 +86,7 @@ import {
8686import { cloneWorkplaneShapeSnapshot , compactEdgeTreatmentHistory , edgeTreatmentAppliedFrame , restoreShapeBeforeEdgeTreatment } from "@/lib/edgeTreatmentHistory" ;
8787import { appendEditorHistorySnapshot , boundedEditorHistoryState , editorHistoryEntry , editorHistoryForExport , hydrateEditorHistoryState , projectShapesFingerprint , type EditorHistoryEntry , type EditorHistoryExportLimit , type EditorHistoryState } from "@/lib/editorHistory" ;
8888import { snapShapeFootprintToVisibleGrid , visibleGridStep } from "@/lib/gridSnap" ;
89+ import { geometryRotationDegreesForShortcut , geometryRotationDelta , rotatedGeometryShapePatch } from "@/lib/geometryRotation" ;
8990import { createLocalId } from "@/lib/localIds" ;
9091import { projectExportFileName } from "@/lib/exportNames" ;
9192import { exportMeshesToObj } from "@/lib/objExport" ;
@@ -8868,10 +8869,14 @@ export function SketchForgeEditor({
88688869 [ commitShapes , hasSelection , placementWorkplane , selectedIds , selectedShapes . length , shapes ] ,
88698870 ) ;
88708871
8871- const rotateSelected45 = useCallback ( ( ) => {
8872+ const rotateSelectedBy = useCallback ( ( angleDegrees : number ) => {
88728873 if ( ! hasSelection ) {
88738874 return ;
88748875 }
8876+ if ( projectInteractionActiveRef . current ) {
8877+ setNotice ( "Finish the current drag or transform before rotating" ) ;
8878+ return ;
8879+ }
88758880
88768881 const selected = new Set ( selectedIds ) ;
88778882 const rotatableShapes = selectedShapes . filter ( ( shape ) => ! shape . locked ) ;
@@ -8880,46 +8885,23 @@ export function SketchForgeEditor({
88808885 return ;
88818886 }
88828887
8883- const axis = new THREE . Vector3 (
8884- placementWorkplane . normal . x ,
8885- placementWorkplane . normal . y ,
8886- placementWorkplane . normal . z ,
8887- ) ;
8888- if ( axis . lengthSq ( ) < 0.000001 ) {
8889- axis . set ( 0 , 1 , 0 ) ;
8890- } else {
8891- axis . normalize ( ) ;
8892- }
8893- const rotationDelta = new THREE . Quaternion ( ) . setFromAxisAngle ( axis , THREE . MathUtils . degToRad ( 45 ) ) ;
8888+ const rotationDelta = geometryRotationDelta ( placementWorkplane , angleDegrees ) ;
88948889 const pivot = rotatableShapes . length > 1 ? selectionCenterOnWorkplane ( rotatableShapes , placementWorkplane ) : null ;
88958890
88968891 const nextShapes = shapes . map ( ( shape ) => {
88978892 if ( ! selected . has ( shape . id ) || shape . locked ) {
88988893 return shape ;
88998894 }
89008895
8901- const nextQuaternion = rotationDelta . clone ( ) . multiply ( quaternionForShape ( shape ) ) ;
8902- const rotationPatch = rotationFromQuaternion ( nextQuaternion ) ;
8903- let rotated = canonicalizeShape ( { ...shape , ...rotationPatch } ) ;
8904-
8905- if ( pivot ) {
8906- const startCenter = new THREE . Vector3 ( shape . x , ( shape . elevation ?? 0 ) + shape . height / 2 , shape . z ) ;
8907- const nextCenter = pivot . clone ( ) . add ( startCenter . sub ( pivot ) . applyQuaternion ( rotationDelta ) ) ;
8908- rotated = canonicalizeShape ( {
8909- ...rotated ,
8910- x : cleanNearZero ( nextCenter . x , 0.0005 ) ,
8911- z : cleanNearZero ( nextCenter . z , 0.0005 ) ,
8912- elevation : cleanNearZero ( nextCenter . y - shape . height / 2 , 0.0005 ) ,
8913- } ) ;
8914- }
8915-
8896+ const rotated = canonicalizeShape ( { ...shape , ...rotatedGeometryShapePatch ( shape , rotationDelta , pivot ) } ) ;
89168897 return canonicalizeShape ( bakeShapeTransformIntoMesh ( rotated ) ) ;
89178898 } ) ;
89188899
8900+ const angleLabel = Number ( angleDegrees . toFixed ( 1 ) ) ;
89198901 commitShapes (
89208902 nextShapes ,
89218903 selectedIds ,
8922- `Rotated ${ rotatableShapes . length } shape${ rotatableShapes . length === 1 ? "" : "s" } by 45 °` ,
8904+ `Rotated ${ rotatableShapes . length } shape${ rotatableShapes . length === 1 ? "" : "s" } by ${ angleLabel } °` ,
89238905 ) ;
89248906 } , [ commitShapes , hasSelection , placementWorkplane , selectedIds , selectedShapes , shapes ] ) ;
89258907
@@ -9052,6 +9034,13 @@ export function SketchForgeEditor({
90529034 return ;
90539035 }
90549036
9037+ const geometryRotationDegrees = geometryRotationDegreesForShortcut ( event ) ;
9038+ if ( geometryRotationDegrees !== null && hasSelection ) {
9039+ event . preventDefault ( ) ;
9040+ rotateSelectedBy ( geometryRotationDegrees ) ;
9041+ return ;
9042+ }
9043+
90559044 const step = event . shiftKey ? 5 : 1 ;
90569045 if ( shortcut && event . key === "ArrowUp" ) {
90579046 event . preventDefault ( ) ;
@@ -9103,6 +9092,7 @@ export function SketchForgeEditor({
91039092 pasteShape ,
91049093 raiseSelected ,
91059094 redo ,
9095+ rotateSelectedBy ,
91069096 rotateSelectedClosedSketch45 ,
91079097 sketchActive ,
91089098 sketchRedo ,
0 commit comments