From cd03c4619610c9426ce46b373975cfddc5e54417 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 17:38:30 +0100 Subject: [PATCH 1/5] Add pixel units and left and right padding options for fitView --- packages/system/src/types/general.ts | 3 ++- packages/system/src/utils/general.ts | 12 +++++++++--- packages/system/src/utils/graph.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 8ac3f9e9..1f6b2ecf 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -95,7 +95,8 @@ export type FitViewParamsBase = { * @inline */ export type FitViewOptionsBase = { - padding?: number; + padding?: number | [number, number]; + paddingUnit?: 'px' | '%'; includeHiddenNodes?: boolean; minZoom?: number; maxZoom?: number; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 32c339e5..90ff64fa 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -195,10 +195,16 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: number + padding: number | [number, number], + paddingUnit: 'px' | '%' ): Viewport => { - const xZoom = width / (bounds.width * (1 + padding)); - const yZoom = height / (bounds.height * (1 + padding)); + const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + + const isPixelPadding = paddingUnit === 'px'; + + const xZoom = width / (isPixelPadding ? bounds.width + paddingX : bounds.width * (1 + paddingX)); + const yZoom = height / (isPixelPadding ? bounds.height + paddingY : bounds.height * (1 + paddingY)); + const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index e14f81cd..930c4adb 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -372,7 +372,8 @@ export async function fitViewport< height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, - options?.padding ?? 0.1 + options?.padding ?? 0.1, + options?.paddingUnit ?? '%' ); await panZoom.setViewport(viewport, { duration: options?.duration }); From 1202c9a68772776a9259c11b8f2ae404b6f16acf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 13:10:13 +0100 Subject: [PATCH 2/5] paddings with units --- packages/system/src/types/general.ts | 18 ++++++- packages/system/src/utils/general.ts | 80 +++++++++++++++++++++++++--- packages/system/src/utils/graph.ts | 3 +- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 1f6b2ecf..9acb3316 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -91,12 +91,26 @@ export type FitViewParamsBase = { maxZoom: number; }; +export type PaddingUnit = 'px' | '%'; +export type PaddingWithUnit = `${number}${PaddingUnit}` | number; + +export type Padding = + | PaddingWithUnit + | [padding: PaddingWithUnit] + | [paddingY: PaddingWithUnit, paddingX: PaddingWithUnit] + | [paddingTop: PaddingWithUnit, paddingX: PaddingWithUnit, paddingBottom: PaddingWithUnit] + | [ + paddingTop: PaddingWithUnit, + paddingRight: PaddingWithUnit, + paddingBottom: PaddingWithUnit, + paddingLeft: PaddingWithUnit + ]; + /** * @inline */ export type FitViewOptionsBase = { - padding?: number | [number, number]; - paddingUnit?: 'px' | '%'; + padding?: Padding; includeHiddenNodes?: boolean; minZoom?: number; maxZoom?: number; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 90ff64fa..a21908bb 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -10,6 +10,8 @@ import type { Transform, InternalNodeBase, NodeLookup, + Padding, + PaddingWithUnit, } from '../types'; import { type Viewport } from '../types'; import { getNodePositionWithOrigin, isInternalNodeBase } from './graph'; @@ -173,6 +175,71 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; +function parsePadding(padding: PaddingWithUnit, viewportDimension: number, bound: number): number { + if (typeof padding === 'number') { + return bound * padding; + } + + if (typeof padding === 'string' && padding.endsWith('px')) { + return parseFloat(padding.slice(0, -2)); + } + + if (typeof padding === 'string' && padding.endsWith('%')) { + return viewportDimension * parseFloat(padding.slice(0, -1)); + } + + return 0; +} + +function parsePaddings( + padding: Padding, + viewportDiemsions: Dimensions, + bounds: Dimensions +): [number, number, number, number] { + if (typeof padding === 'number') { + return [padding, padding, padding, padding]; + } + + if (typeof padding === 'string') { + const paddingX = parsePadding(padding, viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(padding, viewportDiemsions.height, bounds.height); + return [paddingY, paddingX, paddingY, paddingX]; + } + + if (Array.isArray(padding)) { + switch (padding.length) { + case 1: { + const paddingX = parsePadding(padding[0], viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(padding[0], viewportDiemsions.height, bounds.height); + return [paddingY, paddingX, paddingY, paddingX]; + } + case 2: { + const [pY, pX] = padding; + const paddingY = parsePadding(pY, viewportDiemsions.height, bounds.height); + const paddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + return [paddingY, paddingX, paddingY, paddingX]; + } + case 3: { + const [pTop, pX, pBottom] = padding; + const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); + const padddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + return [paddingTop, padddingX, paddingBottom, padddingX]; + } + case 4: { + const [pTop, pRight, pBottom, pLeft] = padding; + const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); + const paddingRight = parsePadding(pRight, viewportDiemsions.width, bounds.width); + const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + const paddingLeft = parsePadding(pLeft, viewportDiemsions.width, bounds.width); + return [paddingTop, paddingRight, paddingBottom, paddingLeft]; + } + } + } + + return [0, 0, 0, 0]; +} + /** * Returns a viewport that encloses the given bounds with optional padding. * @public @@ -195,15 +262,16 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: number | [number, number], - paddingUnit: 'px' | '%' + padding: Padding = 0 ): Viewport => { - const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + // const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; - const isPixelPadding = paddingUnit === 'px'; + // const isPixelPadding = paddingUnit === 'px'; - const xZoom = width / (isPixelPadding ? bounds.width + paddingX : bounds.width * (1 + paddingX)); - const yZoom = height / (isPixelPadding ? bounds.height + paddingY : bounds.height * (1 + paddingY)); + const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, { width, height }, bounds); + + const xZoom = (width - paddingLeft - paddingRight) / bounds.width; + const yZoom = (height - paddingTop - paddingBottom) / bounds.height; const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 930c4adb..e14f81cd 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -372,8 +372,7 @@ export async function fitViewport< height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, - options?.padding ?? 0.1, - options?.paddingUnit ?? '%' + options?.padding ?? 0.1 ); await panZoom.setViewport(viewport, { duration: options?.duration }); From 137b23c6a7bb01cc76f09b3ecb74b928d15998cc Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 17:13:21 +0100 Subject: [PATCH 3/5] add initial support for asymmetric paddings --- examples/react/src/examples/Basic/index.tsx | 3 + packages/system/src/utils/general.ts | 78 +++++++++++---------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index bb53b309..e399ab88 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -146,6 +146,9 @@ const BasicFlow = () => { minZoom={0.2} maxZoom={4} fitView + fitViewOptions={{ + padding: ['30%', '10%', '10%', '20%'], + }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} elevateEdgesOnSelect diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index a21908bb..0d47025b 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -175,63 +175,68 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; -function parsePadding(padding: PaddingWithUnit, viewportDimension: number, bound: number): number { +function paddingError(padding: PaddingWithUnit) { + console.error( + `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` + ); +} + +function parsePadding(padding: PaddingWithUnit, viewport: number): number { if (typeof padding === 'number') { - return bound * padding; + return viewport - viewport / (1 + padding * 0.5); } if (typeof padding === 'string' && padding.endsWith('px')) { - return parseFloat(padding.slice(0, -2)); + const paddingValue = parseFloat(padding); + if (!Number.isNaN(paddingValue)) { + return paddingValue; + } } if (typeof padding === 'string' && padding.endsWith('%')) { - return viewportDimension * parseFloat(padding.slice(0, -1)); + const paddingValue = parseFloat(padding); + if (!Number.isNaN(paddingValue)) { + return viewport * paddingValue * 0.01; + } } + paddingError(padding); return 0; } -function parsePaddings( - padding: Padding, - viewportDiemsions: Dimensions, - bounds: Dimensions -): [number, number, number, number] { - if (typeof padding === 'number') { - return [padding, padding, padding, padding]; - } - - if (typeof padding === 'string') { - const paddingX = parsePadding(padding, viewportDiemsions.width, bounds.width); - const paddingY = parsePadding(padding, viewportDiemsions.height, bounds.height); +function parsePaddings(padding: Padding, width: number, height: number): [number, number, number, number] { + if (typeof padding === 'string' || typeof padding === 'number') { + const paddingY = parsePadding(padding, height); + const paddingX = parsePadding(padding, width); return [paddingY, paddingX, paddingY, paddingX]; } if (Array.isArray(padding)) { switch (padding.length) { case 1: { - const paddingX = parsePadding(padding[0], viewportDiemsions.width, bounds.width); - const paddingY = parsePadding(padding[0], viewportDiemsions.height, bounds.height); + const paddingY = parsePadding(padding[0], height); + const paddingX = parsePadding(padding[0], width); return [paddingY, paddingX, paddingY, paddingX]; } case 2: { const [pY, pX] = padding; - const paddingY = parsePadding(pY, viewportDiemsions.height, bounds.height); - const paddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(pY, height); + const paddingX = parsePadding(pX, width); return [paddingY, paddingX, paddingY, paddingX]; } case 3: { const [pTop, pX, pBottom] = padding; - const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); - const padddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); - const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + const paddingTop = parsePadding(pTop, height); + const padddingX = parsePadding(pX, width); + const paddingBottom = parsePadding(pBottom, height); return [paddingTop, padddingX, paddingBottom, padddingX]; } case 4: { const [pTop, pRight, pBottom, pLeft] = padding; - const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); - const paddingRight = parsePadding(pRight, viewportDiemsions.width, bounds.width); - const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); - const paddingLeft = parsePadding(pLeft, viewportDiemsions.width, bounds.width); + const paddingTop = parsePadding(pTop, height); + const paddingRight = parsePadding(pRight, width); + const paddingBottom = parsePadding(pBottom, height); + const paddingLeft = parsePadding(pLeft, width); return [paddingTop, paddingRight, paddingBottom, paddingLeft]; } } @@ -262,23 +267,24 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: Padding = 0 + padding: Padding ): Viewport => { - // const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, width, height); + const paddingX = paddingLeft + paddingRight; + const paddingY = paddingTop + paddingBottom; - // const isPixelPadding = paddingUnit === 'px'; + console.log({ paddingTop, paddingRight, paddingBottom, paddingLeft }); + console.log(paddingX / paddingLeft); - const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, { width, height }, bounds); - - const xZoom = (width - paddingLeft - paddingRight) / bounds.width; - const yZoom = (height - paddingTop - paddingBottom) / bounds.height; + const xZoom = (width - paddingX) / bounds.width; + const yZoom = (height - paddingY) / bounds.height; const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom; - const y = height / 2 - boundsCenterY * clampedZoom; + const x = width / 2 - boundsCenterX * clampedZoom - paddingX * 0.5 + paddingLeft; + const y = height / 2 - boundsCenterY * clampedZoom - paddingY * 0.5 + paddingTop; return { x, y, zoom: clampedZoom }; }; From e3ecf999aad558b2bc493648f6049da7a1fc87d2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 18:05:58 +0100 Subject: [PATCH 4/5] correctly apply asymetric paddings --- examples/react/src/examples/Basic/index.tsx | 3 +- packages/system/src/utils/general.ts | 35 +++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index e399ab88..6b538593 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -147,7 +147,8 @@ const BasicFlow = () => { maxZoom={4} fitView fitViewOptions={{ - padding: ['30%', '10%', '10%', '20%'], + // top, right, bottom, left + padding: ['100px', '0%', '0%', '50px'], }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 0d47025b..f6b796a7 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -273,9 +273,6 @@ export const getViewportForBounds = ( const paddingX = paddingLeft + paddingRight; const paddingY = paddingTop + paddingBottom; - console.log({ paddingTop, paddingRight, paddingBottom, paddingLeft }); - console.log(paddingX / paddingLeft); - const xZoom = (width - paddingX) / bounds.width; const yZoom = (height - paddingY) / bounds.height; @@ -283,12 +280,38 @@ export const getViewportForBounds = ( const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom - paddingX * 0.5 + paddingLeft; - const y = height / 2 - boundsCenterY * clampedZoom - paddingY * 0.5 + paddingTop; + const x = width / 2 - boundsCenterX * clampedZoom; + const y = height / 2 - boundsCenterY * clampedZoom; - return { x, y, zoom: clampedZoom }; + const realPadding = calculatePadding(bounds, x, y, clampedZoom, width, height); + + const paddingDifferences = { + left: Math.min(Math.floor(realPadding.left) - Math.floor(paddingLeft), 0), + top: Math.min(Math.floor(realPadding.top) - Math.floor(paddingTop), 0), + right: Math.min(Math.floor(realPadding.right) - Math.floor(paddingRight), 0), + bottom: Math.min(Math.floor(realPadding.bottom) - Math.floor(paddingBottom), 0), + }; + return { + x: x - paddingDifferences.left + paddingDifferences.right, + y: y - paddingDifferences.top + paddingDifferences.bottom, + zoom: clampedZoom, + }; }; +function calculatePadding(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { + const { x: paddingLeft, y: paddingTop } = rendererPointToPoint(bounds, [x, y, zoom]); + + const { x: boundRight, y: boundBottom } = rendererPointToPoint( + { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, + [x, y, zoom] + ); + + const paddingRight = width - boundRight; + const paddingBottom = height - boundBottom; + + return { left: paddingLeft, top: paddingTop, right: paddingRight, bottom: paddingBottom }; +} + export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent { From 48d08379392816dcc00c312c5c534d21ef1ee38c Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 26 Mar 2025 12:49:43 +0100 Subject: [PATCH 5/5] use object instead of array for paddings & clean up fitView code --- examples/react/src/examples/Basic/index.tsx | 3 +- packages/system/src/types/general.ts | 17 +-- packages/system/src/utils/general.ts | 161 +++++++++++--------- 3 files changed, 99 insertions(+), 82 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 6b538593..5754fd62 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -147,8 +147,7 @@ const BasicFlow = () => { maxZoom={4} fitView fitViewOptions={{ - // top, right, bottom, left - padding: ['100px', '0%', '0%', '50px'], + padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 }, }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 806b354e..2a34e9ff 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -96,15 +96,14 @@ export type PaddingWithUnit = `${number}${PaddingUnit}` | number; export type Padding = | PaddingWithUnit - | [padding: PaddingWithUnit] - | [paddingY: PaddingWithUnit, paddingX: PaddingWithUnit] - | [paddingTop: PaddingWithUnit, paddingX: PaddingWithUnit, paddingBottom: PaddingWithUnit] - | [ - paddingTop: PaddingWithUnit, - paddingRight: PaddingWithUnit, - paddingBottom: PaddingWithUnit, - paddingLeft: PaddingWithUnit - ]; + | { + top?: PaddingWithUnit; + right?: PaddingWithUnit; + bottom?: PaddingWithUnit; + left?: PaddingWithUnit; + x?: PaddingWithUnit; + y?: PaddingWithUnit; + }; /** * @inline diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index f6b796a7..5b4c6995 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -175,74 +175,103 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; -function paddingError(padding: PaddingWithUnit) { - console.error( - `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` - ); -} - +/** + * Parses a single padding value to a number + * @internal + * @param padding - Padding to parse + * @param viewport - Width or height of the viewport + * @returns The padding in pixels + */ function parsePadding(padding: PaddingWithUnit, viewport: number): number { if (typeof padding === 'number') { - return viewport - viewport / (1 + padding * 0.5); + return Math.floor(viewport - viewport / (1 + padding)); } if (typeof padding === 'string' && padding.endsWith('px')) { const paddingValue = parseFloat(padding); if (!Number.isNaN(paddingValue)) { - return paddingValue; + return Math.floor(paddingValue); } } if (typeof padding === 'string' && padding.endsWith('%')) { const paddingValue = parseFloat(padding); if (!Number.isNaN(paddingValue)) { - return viewport * paddingValue * 0.01; + return Math.floor(viewport * paddingValue * 0.01); } } - paddingError(padding); + console.error( + `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` + ); return 0; } -function parsePaddings(padding: Padding, width: number, height: number): [number, number, number, number] { +/** + * Parses the paddings to an object with top, right, bottom, left, x and y paddings + * @internal + * @param padding - Padding to parse + * @param width - Width of the viewport + * @param height - Height of the viewport + * @returns An object with the paddings in pixels + */ +function parsePaddings( + padding: Padding, + width: number, + height: number +): { top: number; bottom: number; left: number; right: number; x: number; y: number } { if (typeof padding === 'string' || typeof padding === 'number') { const paddingY = parsePadding(padding, height); const paddingX = parsePadding(padding, width); - return [paddingY, paddingX, paddingY, paddingX]; + return { + top: paddingY, + right: paddingX, + bottom: paddingY, + left: paddingX, + x: paddingX * 2, + y: paddingY * 2, + }; } - if (Array.isArray(padding)) { - switch (padding.length) { - case 1: { - const paddingY = parsePadding(padding[0], height); - const paddingX = parsePadding(padding[0], width); - return [paddingY, paddingX, paddingY, paddingX]; - } - case 2: { - const [pY, pX] = padding; - const paddingY = parsePadding(pY, height); - const paddingX = parsePadding(pX, width); - return [paddingY, paddingX, paddingY, paddingX]; - } - case 3: { - const [pTop, pX, pBottom] = padding; - const paddingTop = parsePadding(pTop, height); - const padddingX = parsePadding(pX, width); - const paddingBottom = parsePadding(pBottom, height); - return [paddingTop, padddingX, paddingBottom, padddingX]; - } - case 4: { - const [pTop, pRight, pBottom, pLeft] = padding; - const paddingTop = parsePadding(pTop, height); - const paddingRight = parsePadding(pRight, width); - const paddingBottom = parsePadding(pBottom, height); - const paddingLeft = parsePadding(pLeft, width); - return [paddingTop, paddingRight, paddingBottom, paddingLeft]; - } - } + if (typeof padding === 'object') { + const top = parsePadding(padding.top ?? padding.y ?? 0, height); + const bottom = parsePadding(padding.bottom ?? padding.y ?? 0, height); + const left = parsePadding(padding.left ?? padding.x ?? 0, width); + const right = parsePadding(padding.right ?? padding.x ?? 0, width); + return { top, right, bottom, left, x: left + right, y: top + bottom }; } - return [0, 0, 0, 0]; + return { top: 0, right: 0, bottom: 0, left: 0, x: 0, y: 0 }; +} + +/** + * Calculates the resulting paddings if the new viewport is applied + * @internal + * @param bounds - Bounds to fit inside viewport + * @param x - X position of the viewport + * @param y - Y position of the viewport + * @param zoom - Zoom level of the viewport + * @param width - Width of the viewport + * @param height - Height of the viewport + * @returns An object with the minimum padding required to fit the bounds inside the viewport + */ +function calculateAppliedPaddings(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { + const { x: left, y: top } = rendererPointToPoint(bounds, [x, y, zoom]); + + const { x: boundRight, y: boundBottom } = rendererPointToPoint( + { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, + [x, y, zoom] + ); + + const right = width - boundRight; + const bottom = height - boundBottom; + + return { + left: Math.floor(left), + top: Math.floor(top), + right: Math.floor(right), + bottom: Math.floor(bottom), + }; } /** @@ -258,8 +287,8 @@ function parsePaddings(padding: Padding, width: number, height: number): [number * @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport} * @example * const { x, y, zoom } = getViewportForBounds( - *{ x: 0, y: 0, width: 100, height: 100}, - *1200, 800, 0.5, 2); + * { x: 0, y: 0, width: 100, height: 100}, + * 1200, 800, 0.5, 2); */ export const getViewportForBounds = ( bounds: Rect, @@ -269,49 +298,39 @@ export const getViewportForBounds = ( maxZoom: number, padding: Padding ): Viewport => { - const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, width, height); - const paddingX = paddingLeft + paddingRight; - const paddingY = paddingTop + paddingBottom; + // First we resolve all the paddings to actual pixel values + const p = parsePaddings(padding, width, height); - const xZoom = (width - paddingX) / bounds.width; - const yZoom = (height - paddingY) / bounds.height; + const xZoom = (width - p.x) / bounds.width; + const yZoom = (height - p.y) / bounds.height; + // We calculate the new x, y, zoom for a centered view const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); + const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; const x = width / 2 - boundsCenterX * clampedZoom; const y = height / 2 - boundsCenterY * clampedZoom; - const realPadding = calculatePadding(bounds, x, y, clampedZoom, width, height); + // Then we calculate the minimum padding, to respect asymmetric paddings + const newPadding = calculateAppliedPaddings(bounds, x, y, clampedZoom, width, height); - const paddingDifferences = { - left: Math.min(Math.floor(realPadding.left) - Math.floor(paddingLeft), 0), - top: Math.min(Math.floor(realPadding.top) - Math.floor(paddingTop), 0), - right: Math.min(Math.floor(realPadding.right) - Math.floor(paddingRight), 0), - bottom: Math.min(Math.floor(realPadding.bottom) - Math.floor(paddingBottom), 0), + // We only want to have an offset if the newPadding is smaller than the required padding + const offset = { + left: Math.min(newPadding.left - p.left, 0), + top: Math.min(newPadding.top - p.top, 0), + right: Math.min(newPadding.right - p.right, 0), + bottom: Math.min(newPadding.bottom - p.bottom, 0), }; + return { - x: x - paddingDifferences.left + paddingDifferences.right, - y: y - paddingDifferences.top + paddingDifferences.bottom, + x: x - offset.left + offset.right, + y: y - offset.top + offset.bottom, zoom: clampedZoom, }; }; -function calculatePadding(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { - const { x: paddingLeft, y: paddingTop } = rendererPointToPoint(bounds, [x, y, zoom]); - - const { x: boundRight, y: boundBottom } = rendererPointToPoint( - { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, - [x, y, zoom] - ); - - const paddingRight = width - boundRight; - const paddingBottom = height - boundBottom; - - return { left: paddingLeft, top: paddingTop, right: paddingRight, bottom: paddingBottom }; -} - export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent {