mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 16:51:38 +10:00
327 lines
8.7 KiB
TypeScript
327 lines
8.7 KiB
TypeScript
import Konva from 'konva';
|
|
|
|
const SNAP_THRESHOLD = 10;
|
|
|
|
type SnapPoint = {
|
|
position: number;
|
|
type: 'edge' | 'center';
|
|
direction: 'horizontal' | 'vertical';
|
|
};
|
|
|
|
type SnapResult = {
|
|
x: number;
|
|
y: number;
|
|
horizontalGuide?: number;
|
|
verticalGuide?: number;
|
|
};
|
|
|
|
type ResizeSnapResult = {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
horizontalGuides: number[];
|
|
verticalGuides: number[];
|
|
};
|
|
|
|
export function initializeSnapGuides(stage: Konva.Stage): Konva.Layer {
|
|
// Remove any existing snap guide layers from this stage
|
|
const existingSnapLayers = stage.find('.snap-guide-layer');
|
|
existingSnapLayers.forEach((layer) => layer.destroy());
|
|
|
|
const snapGuideLayer = new Konva.Layer({
|
|
name: 'snap-guide-layer',
|
|
});
|
|
stage.add(snapGuideLayer);
|
|
return snapGuideLayer;
|
|
}
|
|
|
|
export function calculateSnapPositions(
|
|
stage: Konva.Stage,
|
|
excludeId?: string,
|
|
): { horizontal: SnapPoint[]; vertical: SnapPoint[] } {
|
|
const fieldGroups = stage
|
|
.find('.field-group')
|
|
.filter((node): node is Konva.Group => node instanceof Konva.Group);
|
|
const horizontal: SnapPoint[] = [];
|
|
const vertical: SnapPoint[] = [];
|
|
|
|
fieldGroups.forEach((group) => {
|
|
if (excludeId && group.id() === excludeId) {
|
|
return;
|
|
}
|
|
|
|
const rect = group.getClientRect();
|
|
|
|
// Vertical snap points (for horizontal alignment)
|
|
horizontal.push(
|
|
{ position: rect.y, type: 'edge', direction: 'horizontal' },
|
|
{ position: rect.y + rect.height / 2, type: 'center', direction: 'horizontal' },
|
|
{ position: rect.y + rect.height, type: 'edge', direction: 'horizontal' },
|
|
);
|
|
|
|
// Horizontal snap points (for vertical alignment)
|
|
vertical.push(
|
|
{ position: rect.x, type: 'edge', direction: 'vertical' },
|
|
{ position: rect.x + rect.width / 2, type: 'center', direction: 'vertical' },
|
|
{ position: rect.x + rect.width, type: 'edge', direction: 'vertical' },
|
|
);
|
|
});
|
|
|
|
return { horizontal, vertical };
|
|
}
|
|
|
|
export function calculateSnapSizes(
|
|
stage: Konva.Stage,
|
|
excludeId?: string,
|
|
): { widths: number[]; heights: number[] } {
|
|
const fieldGroups = stage
|
|
.find('.field-group')
|
|
.filter((node): node is Konva.Group => node instanceof Konva.Group);
|
|
const widths: number[] = [];
|
|
const heights: number[] = [];
|
|
|
|
fieldGroups.forEach((group) => {
|
|
if (excludeId && group.id() === excludeId) {
|
|
return;
|
|
}
|
|
|
|
const rect = group.getClientRect();
|
|
widths.push(rect.width);
|
|
heights.push(rect.height);
|
|
});
|
|
|
|
return { widths, heights };
|
|
}
|
|
|
|
export function getSnappedPosition(
|
|
stage: Konva.Stage,
|
|
movingGroup: Konva.Group,
|
|
newX: number,
|
|
newY: number,
|
|
): SnapResult {
|
|
const { horizontal, vertical } = calculateSnapPositions(stage, movingGroup.id());
|
|
const rect = movingGroup.getClientRect();
|
|
|
|
let snappedX = newX;
|
|
let snappedY = newY;
|
|
let horizontalGuide: number | undefined;
|
|
let verticalGuide: number | undefined;
|
|
|
|
// Calculate the moving field's snap points
|
|
const movingTop = newY;
|
|
const movingBottom = newY + rect.height;
|
|
const movingCenterY = newY + rect.height / 2;
|
|
const movingLeft = newX;
|
|
const movingRight = newX + rect.width;
|
|
const movingCenterX = newX + rect.width / 2;
|
|
|
|
// Check horizontal snapping (Y position)
|
|
for (const snapPoint of horizontal) {
|
|
const distanceTop = Math.abs(movingTop - snapPoint.position);
|
|
const distanceBottom = Math.abs(movingBottom - snapPoint.position);
|
|
const distanceCenter = Math.abs(movingCenterY - snapPoint.position);
|
|
|
|
if (distanceTop <= SNAP_THRESHOLD) {
|
|
snappedY = snapPoint.position;
|
|
horizontalGuide = snapPoint.position;
|
|
break;
|
|
} else if (distanceBottom <= SNAP_THRESHOLD) {
|
|
snappedY = snapPoint.position - rect.height;
|
|
horizontalGuide = snapPoint.position;
|
|
break;
|
|
} else if (distanceCenter <= SNAP_THRESHOLD) {
|
|
snappedY = snapPoint.position - rect.height / 2;
|
|
horizontalGuide = snapPoint.position;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check vertical snapping (X position)
|
|
for (const snapPoint of vertical) {
|
|
const distanceLeft = Math.abs(movingLeft - snapPoint.position);
|
|
const distanceRight = Math.abs(movingRight - snapPoint.position);
|
|
const distanceCenter = Math.abs(movingCenterX - snapPoint.position);
|
|
|
|
if (distanceLeft <= SNAP_THRESHOLD) {
|
|
snappedX = snapPoint.position;
|
|
verticalGuide = snapPoint.position;
|
|
break;
|
|
} else if (distanceRight <= SNAP_THRESHOLD) {
|
|
snappedX = snapPoint.position - rect.width;
|
|
verticalGuide = snapPoint.position;
|
|
break;
|
|
} else if (distanceCenter <= SNAP_THRESHOLD) {
|
|
snappedX = snapPoint.position - rect.width / 2;
|
|
verticalGuide = snapPoint.position;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: snappedX,
|
|
y: snappedY,
|
|
horizontalGuide,
|
|
verticalGuide,
|
|
};
|
|
}
|
|
|
|
export function getSnappedResize(
|
|
stage: Konva.Stage,
|
|
resizingGroup: Konva.Group,
|
|
newX: number,
|
|
newY: number,
|
|
newWidth: number,
|
|
newHeight: number,
|
|
): ResizeSnapResult {
|
|
const { horizontal, vertical } = calculateSnapPositions(stage, resizingGroup.id());
|
|
const { widths, heights } = calculateSnapSizes(stage, resizingGroup.id());
|
|
|
|
const snappedX = newX;
|
|
const snappedY = newY;
|
|
let snappedWidth = newWidth;
|
|
let snappedHeight = newHeight;
|
|
const horizontalGuides: number[] = [];
|
|
const verticalGuides: number[] = [];
|
|
|
|
// Snap width to other field widths
|
|
for (const width of widths) {
|
|
if (Math.abs(newWidth - width) <= SNAP_THRESHOLD) {
|
|
snappedWidth = width;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Snap height to other field heights
|
|
for (const height of heights) {
|
|
if (Math.abs(newHeight - height) <= SNAP_THRESHOLD) {
|
|
snappedHeight = height;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate field edges with new snapped dimensions
|
|
const movingTop = snappedY;
|
|
const movingBottom = snappedY + snappedHeight;
|
|
const movingLeft = snappedX;
|
|
const movingRight = snappedX + snappedWidth;
|
|
|
|
// Snap edges to alignment guides
|
|
for (const snapPoint of horizontal) {
|
|
if (Math.abs(movingTop - snapPoint.position) <= SNAP_THRESHOLD) {
|
|
horizontalGuides.push(snapPoint.position);
|
|
} else if (Math.abs(movingBottom - snapPoint.position) <= SNAP_THRESHOLD) {
|
|
horizontalGuides.push(snapPoint.position);
|
|
}
|
|
}
|
|
|
|
for (const snapPoint of vertical) {
|
|
if (Math.abs(movingLeft - snapPoint.position) <= SNAP_THRESHOLD) {
|
|
verticalGuides.push(snapPoint.position);
|
|
} else if (Math.abs(movingRight - snapPoint.position) <= SNAP_THRESHOLD) {
|
|
verticalGuides.push(snapPoint.position);
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: snappedX,
|
|
y: snappedY,
|
|
width: snappedWidth,
|
|
height: snappedHeight,
|
|
horizontalGuides,
|
|
verticalGuides,
|
|
};
|
|
}
|
|
|
|
export function showSnapGuides(
|
|
snapGuideLayer: Konva.Layer,
|
|
horizontalGuide?: number,
|
|
verticalGuide?: number,
|
|
stageWidth?: number,
|
|
stageHeight?: number,
|
|
): void {
|
|
if (!snapGuideLayer) {
|
|
return;
|
|
}
|
|
|
|
hideSnapGuides(snapGuideLayer);
|
|
|
|
if (horizontalGuide !== undefined && stageWidth) {
|
|
const horizontalLine = new Konva.Line({
|
|
name: 'snap-guide-horizontal',
|
|
points: [0, horizontalGuide, stageWidth, horizontalGuide],
|
|
stroke: 'rgb(0, 161, 255)',
|
|
strokeWidth: 1,
|
|
dash: [5, 5],
|
|
listening: false,
|
|
});
|
|
snapGuideLayer.add(horizontalLine);
|
|
}
|
|
|
|
if (verticalGuide !== undefined && stageHeight) {
|
|
const verticalLine = new Konva.Line({
|
|
name: 'snap-guide-vertical',
|
|
points: [verticalGuide, 0, verticalGuide, stageHeight],
|
|
stroke: 'rgb(0, 161, 255)',
|
|
strokeWidth: 1,
|
|
dash: [5, 5],
|
|
listening: false,
|
|
});
|
|
snapGuideLayer.add(verticalLine);
|
|
}
|
|
|
|
snapGuideLayer.batchDraw();
|
|
}
|
|
|
|
export function showMultipleSnapGuides(
|
|
snapGuideLayer: Konva.Layer,
|
|
horizontalGuides: number[],
|
|
verticalGuides: number[],
|
|
stageWidth: number,
|
|
stageHeight: number,
|
|
): void {
|
|
if (!snapGuideLayer) {
|
|
return;
|
|
}
|
|
|
|
hideSnapGuides(snapGuideLayer);
|
|
|
|
// Show horizontal guides
|
|
horizontalGuides.forEach((guide) => {
|
|
const horizontalLine = new Konva.Line({
|
|
name: 'snap-guide-horizontal',
|
|
points: [0, guide, stageWidth, guide],
|
|
stroke: 'rgb(0, 161, 255)',
|
|
strokeWidth: 1,
|
|
dash: [5, 5],
|
|
listening: false,
|
|
});
|
|
snapGuideLayer.add(horizontalLine);
|
|
});
|
|
|
|
// Show vertical guides
|
|
verticalGuides.forEach((guide) => {
|
|
const verticalLine = new Konva.Line({
|
|
name: 'snap-guide-vertical',
|
|
points: [guide, 0, guide, stageHeight],
|
|
stroke: 'rgb(0, 161, 255)',
|
|
strokeWidth: 1,
|
|
dash: [5, 5],
|
|
listening: false,
|
|
});
|
|
snapGuideLayer.add(verticalLine);
|
|
});
|
|
|
|
snapGuideLayer.batchDraw();
|
|
}
|
|
|
|
export function hideSnapGuides(snapGuideLayer: Konva.Layer): void {
|
|
if (!snapGuideLayer) {
|
|
return;
|
|
}
|
|
|
|
const guides = snapGuideLayer.find('.snap-guide-horizontal, .snap-guide-vertical');
|
|
guides.forEach((guide: Konva.Node) => guide.destroy());
|
|
snapGuideLayer.batchDraw();
|
|
}
|