Skip to content

Commit 7828f4a

Browse files
authored
feat(core): replace fitView option offset with expressive padding (#1967)
* feat(core): replace fitView option `offset` with expressive `padding` Signed-off-by: braks <[email protected]> * chore(changeset): add * chore: cleanup Signed-off-by: braks <[email protected]> --------- Signed-off-by: braks <[email protected]>
1 parent cc00540 commit 7828f4a

File tree

4 files changed

+169
-18
lines changed

4 files changed

+169
-18
lines changed

.changeset/unlucky-deers-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vue-flow/core": minor
3+
---
4+
5+
Replace the existing `offset` option for fitView with a more expressive `padding` option allowing users to define padding per sides.

packages/core/src/composables/useViewportHelper.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ export function useViewportHelper(state: State) {
160160
options.minZoom ?? state.minZoom,
161161
options.maxZoom ?? state.maxZoom,
162162
options.padding ?? DEFAULT_PADDING,
163-
options.offset,
164163
)
165164

166165
return transformViewport(x, y, zoom, options)
@@ -179,7 +178,7 @@ export function useViewportHelper(state: State) {
179178
state.dimensions.height,
180179
state.minZoom,
181180
state.maxZoom,
182-
options.padding,
181+
options.padding ?? DEFAULT_PADDING,
183182
)
184183

185184
return transformViewport(x, y, zoom, options)

packages/core/src/types/zoom.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,25 @@ export interface TransitionOptions {
2222
interpolate?: 'smooth' | 'linear'
2323
}
2424

25+
export type PaddingUnit = 'px' | '%'
26+
export type PaddingWithUnit = `${number}${PaddingUnit}` | number
27+
28+
export type Padding =
29+
| PaddingWithUnit
30+
| {
31+
top?: PaddingWithUnit
32+
right?: PaddingWithUnit
33+
bottom?: PaddingWithUnit
34+
left?: PaddingWithUnit
35+
x?: PaddingWithUnit
36+
y?: PaddingWithUnit
37+
}
38+
2539
export type FitViewParams = {
26-
padding?: number
40+
padding?: Padding
2741
includeHiddenNodes?: boolean
2842
minZoom?: number
2943
maxZoom?: number
30-
offset?: {
31-
x?: number
32-
y?: number
33-
}
3444
nodes?: string[]
3545
} & TransitionOptions
3646

@@ -45,7 +55,7 @@ export type SetCenterOptions = TransitionOptions & {
4555
}
4656

4757
export type FitBoundsOptions = TransitionOptions & {
48-
padding?: number
58+
padding?: Padding
4959
}
5060

5161
/** Fit the viewport around visible nodes */

packages/core/src/utils/graph.ts

Lines changed: 147 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
442562
export 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

466603
export function getXYZPos(parentPos: XYZPosition, computedPosition: XYZPosition): XYZPosition {

0 commit comments

Comments
 (0)