@@ -16,6 +16,8 @@ import type {
1616 MaybeElement ,
1717 Node ,
1818 NodeLookup ,
19+ Padding ,
20+ PaddingWithUnit ,
1921 Rect ,
2022 ViewportTransform ,
2123 XYPosition ,
@@ -439,28 +441,163 @@ export function getConnectedNodes<N extends Node | { id: string } | string>(node
439441 return nodes . filter ( ( node ) => connectedNodeIds . has ( typeof node === 'string' ? node : node . id ) )
440442}
441443
444+ /**
445+ * Parses a single padding value to a number
446+ * @internal
447+ * @param padding - Padding to parse
448+ * @param viewport - Width or height of the viewport
449+ * @returns The padding in pixels
450+ */
451+ function parsePadding ( padding : PaddingWithUnit , viewport : number ) : number {
452+ if ( typeof padding === 'number' ) {
453+ return Math . floor ( ( viewport - viewport / ( 1 + padding ) ) * 0.5 )
454+ }
455+
456+ if ( typeof padding === 'string' && padding . endsWith ( 'px' ) ) {
457+ const paddingValue = Number . parseFloat ( padding )
458+ if ( ! Number . isNaN ( paddingValue ) ) {
459+ return Math . floor ( paddingValue )
460+ }
461+ }
462+
463+ if ( typeof padding === 'string' && padding . endsWith ( '%' ) ) {
464+ const paddingValue = Number . parseFloat ( padding )
465+ if ( ! Number . isNaN ( paddingValue ) ) {
466+ return Math . floor ( viewport * paddingValue * 0.01 )
467+ }
468+ }
469+
470+ warn ( `The padding value "${ padding } " is invalid. Please provide a number or a string with a valid unit (px or %).` )
471+
472+ return 0
473+ }
474+
475+ /**
476+ * Parses the paddings to an object with top, right, bottom, left, x and y paddings
477+ * @internal
478+ * @param padding - Padding to parse
479+ * @param width - Width of the viewport
480+ * @param height - Height of the viewport
481+ * @returns An object with the paddings in pixels
482+ */
483+ function parsePaddings (
484+ padding : Padding ,
485+ width : number ,
486+ height : number ,
487+ ) : { top : number ; bottom : number ; left : number ; right : number ; x : number ; y : number } {
488+ if ( typeof padding === 'string' || typeof padding === 'number' ) {
489+ const paddingY = parsePadding ( padding , height )
490+ const paddingX = parsePadding ( padding , width )
491+ return {
492+ top : paddingY ,
493+ right : paddingX ,
494+ bottom : paddingY ,
495+ left : paddingX ,
496+ x : paddingX * 2 ,
497+ y : paddingY * 2 ,
498+ }
499+ }
500+
501+ if ( typeof padding === 'object' ) {
502+ const top = parsePadding ( padding . top ?? padding . y ?? 0 , height )
503+ const bottom = parsePadding ( padding . bottom ?? padding . y ?? 0 , height )
504+ const left = parsePadding ( padding . left ?? padding . x ?? 0 , width )
505+ const right = parsePadding ( padding . right ?? padding . x ?? 0 , width )
506+ return { top, right, bottom, left, x : left + right , y : top + bottom }
507+ }
508+
509+ return { top : 0 , right : 0 , bottom : 0 , left : 0 , x : 0 , y : 0 }
510+ }
511+
512+ /**
513+ * Calculates the resulting paddings if the new viewport is applied
514+ * @internal
515+ * @param bounds - Bounds to fit inside viewport
516+ * @param x - X position of the viewport
517+ * @param y - Y position of the viewport
518+ * @param zoom - Zoom level of the viewport
519+ * @param width - Width of the viewport
520+ * @param height - Height of the viewport
521+ * @returns An object with the minimum padding required to fit the bounds inside the viewport
522+ */
523+ function calculateAppliedPaddings ( bounds : Rect , x : number , y : number , zoom : number , width : number , height : number ) {
524+ const { x : left , y : top } = rendererPointToPoint ( bounds , { x, y, zoom } )
525+
526+ const { x : boundRight , y : boundBottom } = rendererPointToPoint (
527+ { x : bounds . x + bounds . width , y : bounds . y + bounds . height } ,
528+ {
529+ x,
530+ y,
531+ zoom,
532+ } ,
533+ )
534+
535+ const right = width - boundRight
536+ const bottom = height - boundBottom
537+
538+ return {
539+ left : Math . floor ( left ) ,
540+ top : Math . floor ( top ) ,
541+ right : Math . floor ( right ) ,
542+ bottom : Math . floor ( bottom ) ,
543+ }
544+ }
545+
546+ /**
547+ * Returns a viewport that encloses the given bounds with padding.
548+ * @public
549+ * @remarks You can determine bounds of nodes with {@link getNodesBounds} and {@link getBoundsOfRects}
550+ * @param bounds - Bounds to fit inside viewport.
551+ * @param width - Width of the viewport.
552+ * @param height - Height of the viewport.
553+ * @param minZoom - Minimum zoom level of the resulting viewport.
554+ * @param maxZoom - Maximum zoom level of the resulting viewport.
555+ * @param padding - Padding around the bounds.
556+ * @returns A transformed {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}.
557+ * @example
558+ * const { x, y, zoom } = getViewportForBounds(
559+ * { x: 0, y: 0, width: 100, height: 100},
560+ * 1200, 800, 0.5, 2);
561+ */
442562export function getTransformForBounds (
443563 bounds : Rect ,
444564 width : number ,
445565 height : number ,
446566 minZoom : number ,
447567 maxZoom : number ,
448- padding = 0.1 ,
449- offset : {
450- x ?: number
451- y ?: number
452- } = { x : 0 , y : 0 } ,
568+ padding : Padding = 0.1 ,
453569) : ViewportTransform {
454- const xZoom = width / ( bounds . width * ( 1 + padding ) )
455- const yZoom = height / ( bounds . height * ( 1 + padding ) )
570+ // First we resolve all the paddings to actual pixel values
571+ const p = parsePaddings ( padding , width , height )
572+
573+ const xZoom = ( width - p . x ) / bounds . width
574+ const yZoom = ( height - p . y ) / bounds . height
575+
576+ // We calculate the new x, y, zoom for a centered view
456577 const zoom = Math . min ( xZoom , yZoom )
457578 const clampedZoom = clamp ( zoom , minZoom , maxZoom )
579+
458580 const boundsCenterX = bounds . x + bounds . width / 2
459581 const boundsCenterY = bounds . y + bounds . height / 2
460- const x = width / 2 - boundsCenterX * clampedZoom + ( offset . x ?? 0 )
461- const y = height / 2 - boundsCenterY * clampedZoom + ( offset . y ?? 0 )
582+ const x = width / 2 - boundsCenterX * clampedZoom
583+ const y = height / 2 - boundsCenterY * clampedZoom
584+
585+ // Then we calculate the minimum padding, to respect asymmetric paddings
586+ const newPadding = calculateAppliedPaddings ( bounds , x , y , clampedZoom , width , height )
587+
588+ // We only want to have an offset if the newPadding is smaller than the required padding
589+ const offset = {
590+ left : Math . min ( newPadding . left - p . left , 0 ) ,
591+ top : Math . min ( newPadding . top - p . top , 0 ) ,
592+ right : Math . min ( newPadding . right - p . right , 0 ) ,
593+ bottom : Math . min ( newPadding . bottom - p . bottom , 0 ) ,
594+ }
462595
463- return { x, y, zoom : clampedZoom }
596+ return {
597+ x : x - offset . left + offset . right ,
598+ y : y - offset . top + offset . bottom ,
599+ zoom : clampedZoom ,
600+ }
464601}
465602
466603export function getXYZPos ( parentPos : XYZPosition , computedPosition : XYZPosition ) : XYZPosition {
0 commit comments