@@ -534,6 +534,7 @@ export class ToolStarterApp {
534534 this . gridRenderEnabled = true ;
535535 this . centerOriginVisible = this . window . sessionStorage ?. getItem ( CENTER_ORIGIN_SESSION_KEY ) !== "0" ;
536536 this . schemaReady = false ;
537+ this . viewportPan = null ;
537538 this . viewport = { ...DEFAULT_VIEWPORT } ;
538539 }
539540
@@ -1033,12 +1034,14 @@ export class ToolStarterApp {
10331034 this . elements . renderSurface . addEventListener ( "dblclick" , ( event ) => this . finishMultiPointDrawing ( "double-click" , event ) ) ;
10341035 this . window . addEventListener ( "pointermove" , ( event ) => this . updatePreviewPointerEdit ( event ) ) ;
10351036 this . window . addEventListener ( "pointermove" , ( event ) => this . updateDrawingPreview ( event ) ) ;
1037+ this . window . addEventListener ( "pointermove" , ( event ) => this . updateViewportPan ( event ) ) ;
10361038 this . elements . renderSurface . addEventListener ( "wheel" , ( event ) => {
10371039 event . preventDefault ( ) ;
10381040 this . zoomViewportByStepAtPointer ( event . deltaY < 0 ? ZOOM_STEP : - ZOOM_STEP , event ) ;
10391041 } , { passive : false } ) ;
10401042 this . window . addEventListener ( "pointerup" , ( event ) => {
10411043 this . isPaintDragging = false ;
1044+ this . finishViewportPan ( event ) ;
10421045 this . finishPreviewPointerEdit ( event ) ;
10431046 } ) ;
10441047 }
@@ -4761,6 +4764,9 @@ export class ToolStarterApp {
47614764 if ( this . startPreviewObjectMove ( event ) ) {
47624765 return ;
47634766 }
4767+ if ( this . startViewportPan ( event ) ) {
4768+ return ;
4769+ }
47644770 this . deselectShape ( "empty canvas" ) ;
47654771 }
47664772 }
@@ -4789,10 +4795,23 @@ export class ToolStarterApp {
47894795 return ! targetIndexes . some ( ( shapeIndex ) => sortedShapes ( object ) [ shapeIndex ] ?. locked ) ;
47904796 }
47914797
4798+ canPanViewportFromEmptyCanvas ( ) {
4799+ return ! this . activeDrawing
4800+ && ! this . previewPointerEdit
4801+ && this . activeTool === "select"
4802+ && this . selectedShapeIndex < 0
4803+ && ! this . selectedShapeIndexes . size
4804+ && ! this . directSelectedShapeIndexes . size
4805+ && ! this . canDragSelectedObjectFromEmptyCanvas ( ) ;
4806+ }
4807+
47924808 updateRenderSurfaceCursorState ( ) {
47934809 const draggingObject = this . previewPointerEdit ?. mode === "move-object" ;
4810+ const panningViewport = Boolean ( this . viewportPan ) ;
47944811 this . elements . renderSurface . classList . toggle ( "is-empty-canvas-object-drag-ready" , this . canDragSelectedObjectFromEmptyCanvas ( ) ) ;
47954812 this . elements . renderSurface . classList . toggle ( "is-empty-canvas-object-dragging" , draggingObject ) ;
4813+ this . elements . renderSurface . classList . toggle ( "is-canvas-pan-ready" , this . canPanViewportFromEmptyCanvas ( ) ) ;
4814+ this . elements . renderSurface . classList . toggle ( "is-canvas-panning" , panningViewport ) ;
47964815 }
47974816
47984817 deselectShape ( sourceLabel = "selection" ) {
@@ -5092,6 +5111,80 @@ export class ToolStarterApp {
50925111 return true ;
50935112 }
50945113
5114+ startViewportPan ( event ) {
5115+ if ( event . button !== 0 || event . shiftKey || event . altKey || event . ctrlKey || event . metaKey || ! this . canPanViewportFromEmptyCanvas ( ) ) {
5116+ return false ;
5117+ }
5118+ event . preventDefault ( ) ;
5119+ event . stopPropagation ( ) ;
5120+ this . viewportPan = {
5121+ dragThresholdMet : false ,
5122+ pointerId : event . pointerId ,
5123+ startClient : {
5124+ x : Number ( event . clientX ) || 0 ,
5125+ y : Number ( event . clientY ) || 0
5126+ } ,
5127+ startViewport : { ...this . viewport }
5128+ } ;
5129+ try {
5130+ this . elements . renderSurface . setPointerCapture ?. ( event . pointerId ) ;
5131+ } catch {
5132+ // Pointer capture is best-effort for synthetic validation events.
5133+ }
5134+ this . updateRenderSurfaceCursorState ( ) ;
5135+ return true ;
5136+ }
5137+
5138+ updateViewportPan ( event ) {
5139+ const pan = this . viewportPan ;
5140+ if ( ! pan || event . buttons !== 1 ) {
5141+ return ;
5142+ }
5143+ const clientX = Number ( event . clientX ) || 0 ;
5144+ const clientY = Number ( event . clientY ) || 0 ;
5145+ const distance = Math . hypot ( clientX - pan . startClient . x , clientY - pan . startClient . y ) ;
5146+ if ( ! pan . dragThresholdMet && distance < PREVIEW_DRAG_START_THRESHOLD_PX ) {
5147+ return ;
5148+ }
5149+ pan . dragThresholdMet = true ;
5150+ const bounds = this . elements . renderSurface . getBoundingClientRect ( ) ;
5151+ if ( ! bounds . width || ! bounds . height ) {
5152+ return ;
5153+ }
5154+ const viewWidth = DEFAULT_VIEWPORT . width / pan . startViewport . zoom ;
5155+ const viewHeight = DEFAULT_VIEWPORT . height / pan . startViewport . zoom ;
5156+ const viewportX = Number ( ( pan . startViewport . x - ( ( clientX - pan . startClient . x ) / bounds . width ) * viewWidth ) . toFixed ( 3 ) ) ;
5157+ const viewportY = Number ( ( pan . startViewport . y - ( ( clientY - pan . startClient . y ) / bounds . height ) * viewHeight ) . toFixed ( 3 ) ) ;
5158+ if ( viewportX === this . viewport . x && viewportY === this . viewport . y ) {
5159+ return ;
5160+ }
5161+ this . viewport . x = viewportX ;
5162+ this . viewport . y = viewportY ;
5163+ this . updateViewport ( ) ;
5164+ this . updateCoordinateDisplay ( event ) ;
5165+ }
5166+
5167+ finishViewportPan ( event ) {
5168+ const pan = this . viewportPan ;
5169+ if ( ! pan ) {
5170+ return false ;
5171+ }
5172+ this . viewportPan = null ;
5173+ try {
5174+ this . elements . renderSurface . releasePointerCapture ?. ( pan . pointerId ) ;
5175+ } catch {
5176+ // Pointer capture is best-effort for synthetic validation events.
5177+ }
5178+ this . updateRenderSurfaceCursorState ( ) ;
5179+ if ( ! pan . dragThresholdMet ) {
5180+ this . deselectShape ( "empty canvas" ) ;
5181+ return true ;
5182+ }
5183+ this . updateCoordinateDisplay ( event ) ;
5184+ this . statusLog . write ( `OK Viewport drag-pan set to ${ this . viewport . x } , ${ this . viewport . y } .` ) ;
5185+ return true ;
5186+ }
5187+
50955188 startPreviewSelectionBoundsMove ( event ) {
50965189 this . startPreviewShapeMove ( event , this . selectedShapeIndex , { fromSelectionBounds : true } ) ;
50975190 }
0 commit comments