From 3374cf4ed5d319ce2cea968f91f744e4e644aa66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 05:51:33 +0000 Subject: [PATCH 1/2] Initial plan From 798bd4de1d4dfff06c6a8db3820c87c25c5665f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 05:58:40 +0000 Subject: [PATCH 2/2] Add width/height snap guides feature - core implementation Co-authored-by: Kitenite <31864905+Kitenite@users.noreply.github.com> --- .../canvas/frame/resize-handles.tsx | 16 +++ .../overlay/elements/snap-guidelines.tsx | 32 ++++- .../src/components/store/editor/snap/index.ts | 128 +++++++++++++++++- .../src/components/store/editor/snap/types.ts | 8 ++ 4 files changed, 179 insertions(+), 5 deletions(-) diff --git a/apps/web/client/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx b/apps/web/client/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx index e7043980b5..d37244260c 100644 --- a/apps/web/client/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx +++ b/apps/web/client/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx @@ -65,6 +65,21 @@ export const ResizeHandles = observer(( newHeight = Math.max(newHeight, minHeight); } + // Check for dimension snapping + const snapTarget = editorEngine.snap.calculateResizeSnapTarget( + frame.id, + frame.position, + { width: newWidth, height: newHeight } + ); + + if (snapTarget) { + newWidth = snapTarget.dimension.width; + newHeight = snapTarget.dimension.height; + editorEngine.snap.showSnapLines(snapTarget.snapLines); + } else { + editorEngine.snap.hideSnapLines(); + } + editorEngine.frames.updateAndSaveToStorage(frame.id, { dimension: { width: Math.round(newWidth), height: Math.round(newHeight) } }); editorEngine.overlay.undebouncedRefresh(); }; @@ -73,6 +88,7 @@ export const ResizeHandles = observer(( e.preventDefault(); e.stopPropagation(); setIsResizing(false); + editorEngine.snap.hideSnapLines(); window.removeEventListener('mousemove', resize as unknown as EventListener); window.removeEventListener('mouseup', stopResize as unknown as EventListener); }; diff --git a/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx b/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx index 9cc5f089a0..f18b1395c7 100644 --- a/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx +++ b/apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/snap-guidelines.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEditorEngine } from '@/components/store/editor'; +import { SnapLineType } from '@/components/store/editor/snap/types'; import { observer } from 'mobx-react-lite'; const SNAP_VISUAL_CONFIG = { @@ -19,6 +20,26 @@ export const SnapGuidelines = observer(() => { const scale = editorEngine.canvas.scale; const canvasPosition = editorEngine.canvas.position; + const getLineColor = (lineType: SnapLineType) => { + switch (lineType) { + case SnapLineType.WIDTH_MATCH: + case SnapLineType.HEIGHT_MATCH: + return 'bg-blue-500'; // Different color for dimension guides + default: + return 'bg-red-500'; // Default color for alignment guides + } + }; + + const getBoxShadow = (lineType: SnapLineType) => { + switch (lineType) { + case SnapLineType.WIDTH_MATCH: + case SnapLineType.HEIGHT_MATCH: + return '0 0 4px rgba(59, 130, 246, 0.6)'; // Blue shadow for dimension guides + default: + return '0 0 4px rgba(239, 68, 68, 0.6)'; // Red shadow for alignment guides + } + }; + return (
{ }} > {snapLines.map((line) => { + const lineColor = getLineColor(line.type); + const boxShadow = getBoxShadow(line.type); + if (line.orientation === 'horizontal') { const visualOffset = (SNAP_VISUAL_CONFIG.TOP_BAR_HEIGHT + SNAP_VISUAL_CONFIG.TOP_BAR_MARGIN) / scale; return (
); @@ -49,14 +73,14 @@ export const SnapGuidelines = observer(() => { return (
); diff --git a/apps/web/client/src/components/store/editor/snap/index.ts b/apps/web/client/src/components/store/editor/snap/index.ts index f634283dd3..b6d6aa1845 100644 --- a/apps/web/client/src/components/store/editor/snap/index.ts +++ b/apps/web/client/src/components/store/editor/snap/index.ts @@ -1,7 +1,7 @@ import type { RectDimension, RectPosition } from '@onlook/models'; import { makeAutoObservable } from 'mobx'; import type { EditorEngine } from '../engine'; -import type { SnapBounds, SnapConfig, SnapFrame, SnapLine, SnapTarget } from './types'; +import type { SnapBounds, SnapConfig, SnapFrame, SnapLine, SnapTarget, ResizeSnapTarget } from './types'; import { SnapLineType } from './types'; const SNAP_CONFIG = { @@ -237,4 +237,130 @@ export class SnapManager { setConfig(config: Partial): void { Object.assign(this.config, config); } + + calculateResizeSnapTarget( + resizeFrameId: string, + currentPosition: RectPosition, + currentDimension: RectDimension, + ): ResizeSnapTarget | null { + if (!this.config.enabled) { + return null; + } + + const otherFrames = this.getSnapFrames(resizeFrameId); + + if (otherFrames.length === 0) { + return null; + } + + const snapCandidates: Array<{ dimension: RectDimension; lines: SnapLine[]; distance: number }> = []; + + for (const otherFrame of otherFrames) { + // Check for width matching + const widthDistance = Math.abs(currentDimension.width - otherFrame.dimension.width); + if (widthDistance <= this.config.threshold) { + const snapLine = this.createDimensionSnapLine( + SnapLineType.WIDTH_MATCH, + 'vertical', + otherFrame, + currentPosition, + currentDimension + ); + + snapCandidates.push({ + dimension: { + width: otherFrame.dimension.width, + height: currentDimension.height + }, + lines: [snapLine], + distance: widthDistance, + }); + } + + // Check for height matching + const heightDistance = Math.abs(currentDimension.height - otherFrame.dimension.height); + if (heightDistance <= this.config.threshold) { + const snapLine = this.createDimensionSnapLine( + SnapLineType.HEIGHT_MATCH, + 'horizontal', + otherFrame, + currentPosition, + currentDimension + ); + + snapCandidates.push({ + dimension: { + width: currentDimension.width, + height: otherFrame.dimension.height + }, + lines: [snapLine], + distance: heightDistance, + }); + } + } + + if (snapCandidates.length === 0) { + return null; + } + + // Find the best candidate (closest match) + snapCandidates.sort((a, b) => a.distance - b.distance); + const bestCandidate = snapCandidates[0]; + + if (!bestCandidate || bestCandidate.distance > this.config.threshold) { + return null; + } + + return { + dimension: bestCandidate.dimension, + snapLines: bestCandidate.lines, + distance: bestCandidate.distance, + }; + } + + private createDimensionSnapLine( + type: SnapLineType, + orientation: 'horizontal' | 'vertical', + otherFrame: SnapFrame, + currentPosition: RectPosition, + currentDimension: RectDimension, + ): SnapLine { + let position: number; + let start: number; + let end: number; + + if (type === SnapLineType.WIDTH_MATCH) { + // Show vertical line at the right edge to indicate width matching + position = Math.max( + currentPosition.x + otherFrame.dimension.width, + otherFrame.position.x + otherFrame.dimension.width + ); + start = Math.min(currentPosition.y, otherFrame.position.y) - SNAP_CONFIG.LINE_EXTENSION; + end = Math.max( + currentPosition.y + currentDimension.height, + otherFrame.position.y + otherFrame.dimension.height + ) + SNAP_CONFIG.LINE_EXTENSION; + } else { + // HEIGHT_MATCH: Show horizontal line at the bottom edge + position = Math.max( + currentPosition.y + otherFrame.dimension.height, + otherFrame.position.y + otherFrame.dimension.height + ); + start = Math.min(currentPosition.x, otherFrame.position.x) - SNAP_CONFIG.LINE_EXTENSION; + end = Math.max( + currentPosition.x + currentDimension.width, + otherFrame.position.x + otherFrame.dimension.width + ) + SNAP_CONFIG.LINE_EXTENSION; + } + + return { + id: `${type}-${otherFrame.id}-${Date.now()}`, + type, + orientation, + position, + start, + end, + frameIds: [otherFrame.id], + }; + } } \ No newline at end of file diff --git a/apps/web/client/src/components/store/editor/snap/types.ts b/apps/web/client/src/components/store/editor/snap/types.ts index 9ccd6185f2..b032ac4147 100644 --- a/apps/web/client/src/components/store/editor/snap/types.ts +++ b/apps/web/client/src/components/store/editor/snap/types.ts @@ -17,6 +17,12 @@ export interface SnapTarget { distance: number; } +export interface ResizeSnapTarget { + dimension: RectDimension; + snapLines: SnapLine[]; + distance: number; +} + export interface SnapLine { id: string; type: SnapLineType; @@ -35,6 +41,8 @@ export enum SnapLineType { CENTER_HORIZONTAL = 'center-horizontal', CENTER_VERTICAL = 'center-vertical', SPACING = 'spacing', + WIDTH_MATCH = 'width-match', + HEIGHT_MATCH = 'height-match', } export interface SnapFrame {