@@ -42,7 +42,7 @@ import {
4242 type PlacementWorkplane ,
4343} from "@/lib/placementWorkplane" ;
4444import { regularPolygonFootprintScale } from "@/lib/regularPolygonFootprint" ;
45- import { DEFAULT_SNAP_GRID , DEFAULT_WORKPLANE_WORKSPACE , normalizeSnapGrid , normalizeWorkspaceSettings , workplaneSettingsFingerprint , workspaceHydrationSyncDecision } from "@/lib/workplaneSettings" ;
45+ import { canBeginShapeDrag , DEFAULT_SNAP_GRID , DEFAULT_WORKPLANE_WORKSPACE , normalizeSnapGrid , normalizeWorkspaceSettings , workplaneSettingsFingerprint , workspaceHydrationSyncDecision } from "@/lib/workplaneSettings" ;
4646import { interiorWorkplaneGridCoordinates , workplaneThemePalette , WORKPLANE_LINE_ELEVATION , WORKPLANE_MAJOR_GRID_INTERVAL } from "@/lib/workplaneGrid" ;
4747import { cleanNearZero , cleanRotationDegrees , fallbackSolidColor , mirroredAxisCount , mirrorSign , preservesEdgeTreatmentSize , proportionalResizeScale , resizedImportedCoordinates , resizedImportedMeshPositions , resizedShapeSize , shapeDepth , shapeHasTaper , shapeOverallFootprintDimensions , shapeTaperDimensions , shapeTaperScaleAt , shapeWidth } from "@/lib/workplaneShapes" ;
4848import { sphereTessellation } from "@/lib/sphereTessellation" ;
@@ -51,6 +51,7 @@ import {
5151 TransformOverlay ,
5252 getElevationMeasureKey ,
5353 measureKeyForHandle ,
54+ normalizedRotationPlaneBasis ,
5455 type DimensionMark ,
5556 type EditingDimension ,
5657 type EditingRotation ,
@@ -1837,8 +1838,6 @@ function signedAngleAroundAxis(start: THREE.Vector3, current: THREE.Vector3, axi
18371838
18381839const ROTATION_HANDLE_SIDE_HYSTERESIS = 0.22 ;
18391840const ROTATION_HANDLE_DOMINANCE_HYSTERESIS = 0.18 ;
1840- const ROTATION_UPPER_HANDLE_ICON_ANGLE = 0 ;
1841- const ROTATION_BOTTOM_HANDLE_ICON_ANGLE = 0 ;
18421841function signedRotationSide ( value : number , previous : RotationHandleSide | undefined , positiveSide : RotationHandleSide , negativeSide : RotationHandleSide ) {
18431842 if ( previous === positiveSide && value > - ROTATION_HANDLE_SIDE_HYSTERESIS ) {
18441843 return previous ;
@@ -1896,9 +1895,10 @@ function rotationHandleSidesForCamera(
18961895 const viewZ = viewZRaw / length ;
18971896 const previous = state . rotationHandleSides ?? undefined ;
18981897 const next : RotationHandleSides = {
1899- x : signedRotationSide ( viewX , previous ?. x , "right" , "left" ) ,
1898+ // Keep the two upper rotation handles on the faces opposite the camera.
1899+ x : signedRotationSide ( viewX , previous ?. x , "left" , "right" ) ,
19001900 y : dominantRotationSide ( viewX , viewZ , previous ?. y ) ,
1901- z : signedRotationSide ( viewZ , previous ?. z , "near " , "far " ) ,
1901+ z : signedRotationSide ( viewZ , previous ?. z , "far " , "near " ) ,
19021902 } ;
19031903 state . rotationHandleSides = next ;
19041904 return next ;
@@ -4262,6 +4262,9 @@ export function WorkplaneViewport({
42624262 if ( ! alreadySelected ) {
42634263 onSelectShape ( id ) ;
42644264 }
4265+ if ( ! canBeginShapeDrag ( workspaceRef . current . selectBeforeMove , alreadySelected ) ) {
4266+ return ;
4267+ }
42654268 if ( shape . locked ) {
42664269 return ;
42674270 }
@@ -6231,7 +6234,10 @@ function updateTransformOverlayDom(state: ThreeState, next: TransformOverlayStat
62316234 }
62326235 element . style . setProperty ( "--overlay-x" , `${ handle . x } px` ) ;
62336236 element . style . setProperty ( "--overlay-y" , `${ handle . y } px` ) ;
6234- element . style . setProperty ( "--rotate-handle-angle" , `${ handle . angle } deg` ) ;
6237+ element . style . setProperty ( "--rotate-plane-a" , String ( handle . plane . a ) ) ;
6238+ element . style . setProperty ( "--rotate-plane-b" , String ( handle . plane . b ) ) ;
6239+ element . style . setProperty ( "--rotate-plane-c" , String ( handle . plane . c ) ) ;
6240+ element . style . setProperty ( "--rotate-plane-d" , String ( handle . plane . d ) ) ;
62356241 } ) ;
62366242}
62376243
@@ -6438,37 +6444,34 @@ function syncTransformOverlay(
64386444 } ;
64396445 const rotateLeft = screenOffsetFromCenter ( project ( sidePoint ( rotationSides . x , frame . max . y ) ) , 24 ) ;
64406446 const rotateRight = screenOffsetFromCenter ( project ( sidePoint ( rotationSides . z , frame . max . y ) ) , 28 ) ;
6441- const rotateBottom = screenOffsetFromCenter ( project ( sidePoint ( rotationSides . y , footprintY ) ) , 34 ) ;
6447+ const rotateBottomAnchor = screenOffsetFromCenter ( project ( sidePoint ( rotationSides . y , footprintY ) ) , 26 ) ;
6448+ const rotateBottom = { ...rotateBottomAnchor , y : rotateBottomAnchor . y - 5 } ;
64426449 const xFaceCenter = sidePoint ( rotationSides . x , 0 ) ;
64436450 const zFaceCenter = sidePoint ( rotationSides . z , 0 ) ;
64446451 const yFaceCenter = bottomCenterWorld ;
6445- const projectedAxisAngle = ( centerWorld : THREE . Vector3 , axis : THREE . Vector3 ) => {
6446- const from = project ( centerWorld . clone ( ) . addScaledVector ( axis , - 1 ) ) ;
6447- const to = project ( centerWorld . clone ( ) . addScaledVector ( axis , 1 ) ) ;
6448- return THREE . MathUtils . radToDeg ( Math . atan2 ( to . y - from . y , to . x - from . x ) ) ;
6449- } ;
6450- const rotateWithWorkplane = ! placementWorkplaneIsBase ( activeWorkplane ) ;
6451- const xRotateAngle = rotateWithWorkplane
6452- ? projectedAxisAngle ( xFaceCenter , zFootAxis )
6453- : ROTATION_UPPER_HANDLE_ICON_ANGLE ;
6454- const zRotateAngle = rotateWithWorkplane
6455- ? projectedAxisAngle ( zFaceCenter , xFootAxis )
6456- : ROTATION_UPPER_HANDLE_ICON_ANGLE ;
6457- const yRotateTangent = rotationSides . y === "right" || rotationSides . y === "left"
6458- ? zFootAxis
6459- : xFootAxis ;
6460- const yRotateAngle = rotateWithWorkplane
6461- ? projectedAxisAngle ( yFaceCenter , yRotateTangent )
6462- : ROTATION_BOTTOM_HANDLE_ICON_ANGLE ;
6452+ const yRotationAxes = rotationSides . y === "near"
6453+ ? { u : xFootAxis , v : zFootAxis }
6454+ : rotationSides . y === "far"
6455+ ? { u : xFootAxis . clone ( ) . multiplyScalar ( - 1 ) , v : zFootAxis . clone ( ) . multiplyScalar ( - 1 ) }
6456+ : rotationSides . y === "right"
6457+ ? { u : zFootAxis . clone ( ) . multiplyScalar ( - 1 ) , v : xFootAxis }
6458+ : { u : zFootAxis , v : xFootAxis . clone ( ) . multiplyScalar ( - 1 ) } ;
64636459 const planeRadius = 154 ;
6464- const planeWorldStep = Math . max ( 12 , Math . max ( frame . width , frame . depth , frame . height ) * 0.78 ) ;
6460+ const planeWorldStep = Math . max ( 0.1 , cameraDistance * 0.01 ) ;
64656461 const makePlaneView = ( centerWorld : THREE . Vector3 , uAxis : THREE . Vector3 , vAxis : THREE . Vector3 ) : RotationPlaneView => {
64666462 const screenCenter = project ( centerWorld ) ;
6467- const u = project ( centerWorld . clone ( ) . add ( uAxis . clone ( ) . multiplyScalar ( planeWorldStep ) ) ) ;
6468- const v = project ( centerWorld . clone ( ) . add ( vAxis . clone ( ) . multiplyScalar ( planeWorldStep ) ) ) ;
6469- const du = { x : u . x - screenCenter . x , y : u . y - screenCenter . y } ;
6470- const dv = { x : v . x - screenCenter . x , y : v . y - screenCenter . y } ;
6471- const longest = Math . max ( 12 , Math . hypot ( du . x , du . y ) , Math . hypot ( dv . x , dv . y ) ) ;
6463+ const uOffset = uAxis . clone ( ) . multiplyScalar ( planeWorldStep ) ;
6464+ const vOffset = vAxis . clone ( ) . multiplyScalar ( planeWorldStep ) ;
6465+ const uStart = project ( centerWorld . clone ( ) . sub ( uOffset ) ) ;
6466+ const uEnd = project ( centerWorld . clone ( ) . add ( uOffset ) ) ;
6467+ const vStart = project ( centerWorld . clone ( ) . sub ( vOffset ) ) ;
6468+ const vEnd = project ( centerWorld . clone ( ) . add ( vOffset ) ) ;
6469+ const du = { x : ( uEnd . x - uStart . x ) / 2 , y : ( uEnd . y - uStart . y ) / 2 } ;
6470+ const dv = { x : ( vEnd . x - vStart . x ) / 2 , y : ( vEnd . y - vStart . y ) / 2 } ;
6471+ const longest = Math . max ( Math . hypot ( du . x , du . y ) , Math . hypot ( dv . x , dv . y ) ) ;
6472+ if ( ! Number . isFinite ( longest ) || longest < 0.000001 ) {
6473+ return { x : screenCenter . x , y : screenCenter . y , a : 1 , b : 0 , c : 0 , d : 1 } ;
6474+ }
64726475 const scale = planeRadius / longest / 100 ;
64736476 return {
64746477 x : screenCenter . x ,
@@ -6499,6 +6502,19 @@ function syncTransformOverlay(
64996502 y : makePlaneView ( yFaceCenter , xFootAxis , zFootAxis ) ,
65006503 z : makePlaneView ( zFaceCenter , xFootAxis , yFootAxis ) ,
65016504 } ;
6505+ const makeCameraPlaneView = ( uAxis : THREE . Vector3 , vAxis : THREE . Vector3 ) : RotationPlaneView => {
6506+ const u = uAxis . clone ( ) . transformDirection ( state . camera . matrixWorldInverse ) ;
6507+ const v = vAxis . clone ( ) . transformDirection ( state . camera . matrixWorldInverse ) ;
6508+ return { x : 0 , y : 0 , a : u . x , b : - u . y , c : v . x , d : - v . y } ;
6509+ } ;
6510+ const rotationHandlePlanes : Record < RotationAxis , RotationPlaneView > = {
6511+ // Project only the two plane axes through the camera rotation. Ignoring
6512+ // perspective translation keeps glyph appearance independent of object
6513+ // length and screen position while preserving camera foreshortening.
6514+ x : makeCameraPlaneView ( zFootAxis , yFootAxis ) ,
6515+ y : makeCameraPlaneView ( yRotationAxes . u , yRotationAxes . v ) ,
6516+ z : makeCameraPlaneView ( xFootAxis , yFootAxis ) ,
6517+ } ;
65026518
65036519 const next = {
65046520 id : frame . ids . join ( "|" ) ,
@@ -6521,9 +6537,9 @@ function syncTransformOverlay(
65216537 { key : liftHandleKey , className : showLowerHandles ? "height-lift lower" : "height-lift" , kind : "lift" as const , x : liftPoint . x , y : liftPoint . y , title : "Lift" , angle : liftHandleAngle } ,
65226538 ] ,
65236539 rotateHandles : [
6524- { key : "rotate-left" , className : "screen-left" , x : rotateLeft . x , y : rotateLeft . y , angle : xRotateAngle } ,
6525- { key : "rotate-right" , className : "screen-right" , x : rotateRight . x , y : rotateRight . y , angle : zRotateAngle } ,
6526- { key : "rotate-bottom" , className : "screen-bottom" , x : rotateBottom . x , y : rotateBottom . y , angle : yRotateAngle } ,
6540+ { key : "rotate-left" , className : "screen-left" , x : rotateLeft . x , y : rotateLeft . y , plane : normalizedRotationPlaneBasis ( rotationHandlePlanes . x , true ) } ,
6541+ { key : "rotate-right" , className : "screen-right" , x : rotateRight . x , y : rotateRight . y , plane : normalizedRotationPlaneBasis ( rotationHandlePlanes . z , true ) } ,
6542+ { key : "rotate-bottom" , className : "screen-bottom" , x : rotateBottom . x , y : rotateBottom . y , plane : normalizedRotationPlaneBasis ( rotationHandlePlanes . y , true ) } ,
65276543 ] ,
65286544 dimensions : dimensionMarks ,
65296545 rotationWheel : rotationWheels . y ,
0 commit comments