diff --git a/blocksuite/affine/block-root/src/edgeless/edgeless-builtin-spec.ts b/blocksuite/affine/block-root/src/edgeless/edgeless-builtin-spec.ts index 266df1533b..89a0a7eec5 100644 --- a/blocksuite/affine/block-root/src/edgeless/edgeless-builtin-spec.ts +++ b/blocksuite/affine/block-root/src/edgeless/edgeless-builtin-spec.ts @@ -21,7 +21,7 @@ import { ShapeTool } from './gfx-tool/shape-tool.js'; import { TemplateTool } from './gfx-tool/template-tool.js'; import { TextTool } from './gfx-tool/text-tool.js'; import { EditPropsMiddlewareBuilder } from './middlewares/base.js'; -import { EdgelessSnapManager } from './utils/snap-manager.js'; +import { SnapManager } from './utils/snap-manager.js'; export const EdgelessToolExtension: ExtensionType[] = [ DefaultTool, @@ -43,7 +43,7 @@ export const EdgelessBuiltInManager: ExtensionType[] = [ ConnectionOverlay, FrameOverlay, MindMapIndicatorOverlay, - EdgelessSnapManager, + SnapManager, EdgelessFrameManager, EditPropsMiddlewareBuilder, ]; diff --git a/blocksuite/affine/block-root/src/edgeless/gfx-tool/default-tool.ts b/blocksuite/affine/block-root/src/edgeless/gfx-tool/default-tool.ts index 66ce37838b..3d4199aae2 100644 --- a/blocksuite/affine/block-root/src/edgeless/gfx-tool/default-tool.ts +++ b/blocksuite/affine/block-root/src/edgeless/gfx-tool/default-tool.ts @@ -58,7 +58,7 @@ import { prepareCloneData } from '../utils/clone-utils.js'; import { isSingleMindMapNode } from '../utils/mindmap.js'; import { calPanDelta } from '../utils/panning-utils.js'; import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js'; -import type { EdgelessSnapManager } from '../utils/snap-manager.js'; +import type { SnapManager } from '../utils/snap-manager.js'; import { addText, mountConnectorLabelEditor, @@ -277,9 +277,7 @@ export class DefaultTool extends BaseTool { } get snapOverlay() { - return this.std.get( - OverlayIdentifier('snap-manager') - ) as EdgelessSnapManager; + return this.std.get(OverlayIdentifier('snap-manager')) as SnapManager; } private _addEmptyParagraphBlock( @@ -580,7 +578,7 @@ export class DefaultTool extends BaseTool { ) { const mindmap = this._toBeMoved[0].group as MindmapElementModel; - this._alignBound = this.snapOverlay.setupAlignables(this._toBeMoved, [ + this._alignBound = this.snapOverlay.setMovingElements(this._toBeMoved, [ mindmap, ...(mindmap?.childElements || []), ]); @@ -640,7 +638,7 @@ export class DefaultTool extends BaseTool { ); } - this._alignBound = this.snapOverlay.setupAlignables( + this._alignBound = this.snapOverlay.setMovingElements( this._toBeMoved ); @@ -882,7 +880,7 @@ export class DefaultTool extends BaseTool { if (this.edgelessSelectionManager.editing) return; this._selectedBounds = []; - this.snapOverlay.cleanupAlignables(); + this.snapOverlay.clear(); this.frameOverlay.clear(); this._toBeMoved = []; this._selectedConnector = null; diff --git a/blocksuite/affine/block-root/src/edgeless/utils/snap-manager.ts b/blocksuite/affine/block-root/src/edgeless/utils/snap-manager.ts index dc1f6d91c8..6352176b23 100644 --- a/blocksuite/affine/block-root/src/edgeless/utils/snap-manager.ts +++ b/blocksuite/affine/block-root/src/edgeless/utils/snap-manager.ts @@ -1,30 +1,49 @@ -import type { - SurfaceBlockComponent, - SurfaceBlockModel, -} from '@blocksuite/affine-block-surface'; -import { getSurfaceBlock, Overlay } from '@blocksuite/affine-block-surface'; -import type { ConnectorElementModel } from '@blocksuite/affine-model'; -import type { GfxController, GfxModel } from '@blocksuite/block-std/gfx'; -import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; -import { Bound, Point } from '@blocksuite/global/utils'; - -import { isConnectable } from '../utils/query.js'; +import { Overlay } from '@blocksuite/affine-block-surface'; +import { ConnectorElementModel } from '@blocksuite/affine-model'; +import type { GfxModel } from '@blocksuite/block-std/gfx'; +import { almostEqual, Bound, Point } from '@blocksuite/global/utils'; interface Distance { - absXDistance: number; - absYDistance: number; - xDistance: number; - yDistance: number; - indexX: number; - indexY: number; + horiz?: { + /** + * the minimum x moving distance to align with other bound + */ + distance: number; + + /** + * the indices of the align position + */ + alignPositionIndices: number[]; + }; + + vert?: { + /** + * the minimum y moving distance to align with other bound + */ + distance: number; + + /** + * the indices of the align position + */ + alignPositionIndices: number[]; + }; } -const ALIGN_THRESHOLD = 5; +const ALIGN_THRESHOLD = 8; +const DISTRIBUTION_LINE_OFFSET = 1; -export class EdgelessSnapManager extends Overlay { +export class SnapManager extends Overlay { static override overlayName: string = 'snap-manager'; - private _alignableBounds: Bound[] = []; + private _referenceBounds: { + vertical: Bound[]; + horizontal: Bound[]; + all: Bound[]; + } = { + vertical: [], + horizontal: [], + all: [], + }; /** * This variable contains reference lines that are @@ -44,28 +63,16 @@ export class EdgelessSnapManager extends Overlay { */ private _intraGraphicAlignLines: [Point, Point][] = []; - cleanupAlignables = () => { - this._alignableBounds = []; + override clear() { + super.clear(); + + this._referenceBounds = { + vertical: [], + horizontal: [], + all: [], + }; this._intraGraphicAlignLines = []; this._distributedAlignLines = []; - // FIXME: not sure why renderer can be undefined sometimes - this._surface.renderer?.removeOverlay(this); - }; - - private get _surface() { - const surfaceModel = getSurfaceBlock(this.gfx.doc); - if (!surfaceModel) { - throw new BlockSuiteError( - ErrorCode.ValueNotExists, - 'Surface block not found in doc when creating snap manager' - ); - } - - return this.gfx.std.view.getBlock(surfaceModel.id) as SurfaceBlockComponent; - } - - constructor(gfx: GfxController) { - super(gfx); } private _alignDistributeHorizontally( @@ -75,31 +82,52 @@ export class EdgelessSnapManager extends Overlay { viewport: { zoom: number } ) { const wBoxes: Bound[] = []; - this._alignableBounds.forEach(box => { + this._referenceBounds.horizontal.forEach(box => { if (box.isHorizontalCross(bound)) { wBoxes.push(box); } }); + + wBoxes.sort((a, b) => a.center[0] - b.center[0]); + let dif = Infinity; let min = Infinity; + let aveDis = Number.MAX_SAFE_INTEGER; + let curBound!: { + leftIdx: number; + rightIdx: number; + spacing: number; + points: [Point, Point][]; + }; for (let i = 0; i < wBoxes.length; i++) { for (let j = i + 1; j < wBoxes.length; j++) { let lb = wBoxes[i], rb = wBoxes[j]; // it means these bound need to be horizontally across - if (!lb.isHorizontalCross(rb)) continue; - if (lb.isIntersectWithBound(rb)) continue; + if (!lb.isHorizontalCross(rb) || lb.isIntersectWithBound(rb)) continue; + + let switchFlag = false; + // exchange lb and rb to make sure lb is on the left of rb if (rb.maxX < lb.minX) { const temp = rb; rb = lb; lb = temp; + switchFlag = true; } - /** align middle */ + let _centerX = 0; const updateDif = () => { dif = Math.abs(bound.center[0] - _centerX); - if (dif <= threshold && dif < min) { + const curAveDis = + (Math.abs(lb.center[0] - bound.center[0]) + + Math.abs(rb.center[0] - bound.center[0])) / + 2; + if ( + dif <= threshold && + (dif < min || (almostEqual(dif, min) && curAveDis < aveDis)) + ) { min = dif; + aveDis = curAveDis; rst.dx = _centerX - bound.center[0]; /** * calculate points to draw @@ -108,37 +136,102 @@ export class EdgelessSnapManager extends Overlay { (a, b) => a - b ); const y = (ys[1] + ys[2]) / 2; - const offset = 2 / viewport.zoom; + const offset = DISTRIBUTION_LINE_OFFSET / viewport.zoom; const xs = [ - _centerX - bound.w / 2 - offset, - _centerX + bound.w / 2 + offset, + _centerX - bound.w / 2, + _centerX + bound.w / 2, rb.minX, rb.maxX, lb.minX, lb.maxX, ].sort((a, b) => a - b); - this._distributedAlignLines[0] = [ - new Point(xs[1], y), - new Point(xs[2], y), - ]; - this._distributedAlignLines[1] = [ - new Point(xs[3], y), - new Point(xs[4], y), - ]; + + curBound = { + leftIdx: switchFlag ? j : i, + rightIdx: switchFlag ? i : j, + spacing: xs[2] - xs[1], + points: [ + [new Point(xs[1] + offset, y), new Point(xs[2] - offset, y)], + [new Point(xs[3] + offset, y), new Point(xs[4] - offset, y)], + ], + }; } }; + + /** + * align between left and right bound + */ if (lb.horizontalDistance(rb) > bound.w) { _centerX = (lb.maxX + rb.minX) / 2; updateDif(); } - /** align left */ + + /** + * align to the left bounds + */ _centerX = lb.minX - (rb.minX - lb.maxX) - bound.w / 2; updateDif(); + /** align right */ _centerX = rb.minX - lb.maxX + rb.maxX + bound.w / 2; updateDif(); } } + + // find the boxes that has same spacing + if (curBound) { + const { leftIdx, rightIdx, spacing, points } = curBound; + + this._distributedAlignLines.push(...points); + + { + let curLeftBound = wBoxes[leftIdx]; + + for (let i = leftIdx - 1; i >= 0; i--) { + if (almostEqual(wBoxes[i].maxX, curLeftBound.minX - spacing)) { + const targetBound = wBoxes[i]; + const ys = [ + targetBound.minY, + targetBound.maxY, + curLeftBound.minY, + curLeftBound.maxY, + ].sort((a, b) => a - b); + const y = (ys[1] + ys[2]) / 2; + + this._distributedAlignLines.push([ + new Point(wBoxes[i].maxX, y), + new Point(curLeftBound.minX, y), + ]); + + curLeftBound = wBoxes[i]; + } + } + } + + { + let curRightBound = wBoxes[rightIdx]; + + for (let i = rightIdx + 1; i < wBoxes.length; i++) { + if (almostEqual(wBoxes[i].minX, curRightBound.maxX + spacing)) { + const targetBound = wBoxes[i]; + const ys = [ + targetBound.minY, + targetBound.maxY, + curRightBound.minY, + curRightBound.maxY, + ].sort((a, b) => a - b); + const y = (ys[1] + ys[2]) / 2; + + this._distributedAlignLines.push([ + new Point(curRightBound.maxX, y), + new Point(wBoxes[i].minX, y), + ]); + + curRightBound = wBoxes[i]; + } + } + } + } } private _alignDistributeVertically( @@ -148,29 +241,50 @@ export class EdgelessSnapManager extends Overlay { viewport: { zoom: number } ) { const hBoxes: Bound[] = []; - this._alignableBounds.forEach(box => { + this._referenceBounds.vertical.forEach(box => { if (box.isVerticalCross(bound)) { hBoxes.push(box); } }); + + hBoxes.sort((a, b) => a.center[0] - b.center[0]); + let dif = Infinity; let min = Infinity; + let aveDis = Number.MAX_SAFE_INTEGER; + let curBound!: { + upperIdx: number; + lowerIdx: number; + spacing: number; + points: [Point, Point][]; + }; for (let i = 0; i < hBoxes.length; i++) { for (let j = i + 1; j < hBoxes.length; j++) { let ub = hBoxes[i], db = hBoxes[j]; - if (!ub.isVerticalCross(db)) continue; - if (ub.isIntersectWithBound(db)) continue; + if (!ub.isVerticalCross(db) || ub.isIntersectWithBound(db)) continue; + + let switchFlag = false; if (db.maxY < ub.minX) { const temp = ub; ub = db; db = temp; + switchFlag = true; } + /** align middle */ let _centerY = 0; const updateDiff = () => { dif = Math.abs(bound.center[1] - _centerY); - if (dif <= threshold && dif < min) { + const curAveDis = + (Math.abs(ub.center[1] - bound.center[1]) + + Math.abs(db.center[1] - bound.center[1])) / + 2; + + if ( + dif <= threshold && + (dif < min || (almostEqual(dif, min) && curAveDis < aveDis)) + ) { min = dif; rst.dy = _centerY - bound.center[1]; /** @@ -180,29 +294,33 @@ export class EdgelessSnapManager extends Overlay { (a, b) => a - b ); const x = (xs[1] + xs[2]) / 2; - const offset = 2 / viewport.zoom; + const offset = DISTRIBUTION_LINE_OFFSET / viewport.zoom; const ys = [ - _centerY - bound.h / 2 - offset, - _centerY + bound.h / 2 + offset, + _centerY - bound.h / 2, + _centerY + bound.h / 2, db.minY, db.maxY, ub.minY, ub.maxY, ].sort((a, b) => a - b); - this._distributedAlignLines[3] = [ - new Point(x, ys[1]), - new Point(x, ys[2]), - ]; - this._distributedAlignLines[4] = [ - new Point(x, ys[3]), - new Point(x, ys[4]), - ]; + + curBound = { + upperIdx: switchFlag ? j : i, + lowerIdx: switchFlag ? i : j, + spacing: ys[2] - ys[1], + points: [ + [new Point(x, ys[1] + offset), new Point(x, ys[2] - offset)], + [new Point(x, ys[3] + offset), new Point(x, ys[4] - offset)], + ], + }; } }; + if (ub.verticalDistance(db) > bound.h) { _centerY = (ub.maxY + db.minY) / 2; updateDiff(); } + /** align upper */ _centerY = ub.minY - (db.minY - ub.maxY) - bound.h / 2; updateDiff(); @@ -211,6 +329,61 @@ export class EdgelessSnapManager extends Overlay { updateDiff(); } } + + // find the boxes that has same spacing + if (curBound) { + const { upperIdx, lowerIdx, spacing, points } = curBound; + + this._distributedAlignLines.push(...points); + + { + let curUpperBound = hBoxes[upperIdx]; + + for (let i = upperIdx - 1; i >= 0; i--) { + if (almostEqual(hBoxes[i].maxY, curUpperBound.minY - spacing)) { + const targetBound = hBoxes[i]; + const xs = [ + targetBound.minX, + targetBound.maxX, + curUpperBound.minX, + curUpperBound.maxX, + ].sort((a, b) => a - b); + const x = (xs[1] + xs[2]) / 2; + + this._distributedAlignLines.push([ + new Point(x, hBoxes[i].maxY), + new Point(x, curUpperBound.minY), + ]); + + curUpperBound = hBoxes[i]; + } + } + } + + { + let curLowerBound = hBoxes[lowerIdx]; + + for (let i = lowerIdx + 1; i < hBoxes.length; i++) { + if (almostEqual(hBoxes[i].minY, curLowerBound.maxY + spacing)) { + const targetBound = hBoxes[i]; + const xs = [ + targetBound.minX, + targetBound.maxX, + curLowerBound.minX, + curLowerBound.maxX, + ].sort((a, b) => a - b); + const x = (xs[1] + xs[2]) / 2; + + this._distributedAlignLines.push([ + new Point(x, curLowerBound.maxY), + new Point(x, hBoxes[i].minY), + ]); + + curLowerBound = hBoxes[i]; + } + } + } + } } private _calculateClosestDistances(bound: Bound, other: Bound): Distance { @@ -263,94 +436,150 @@ export class EdgelessSnapManager extends Overlay { const closestX = Math.min(...xDistancesAbs); const closestY = Math.min(...yDistancesAbs); - const indexX = xDistancesAbs.indexOf(closestX); - const indexY = yDistancesAbs.indexOf(closestY); + const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom; // the x and y distances will be useful for locating the align point return { - absXDistance: closestX, - absYDistance: closestY, - xDistance: xDistances[indexX], - yDistance: yDistances[indexY], - indexX, - indexY, + horiz: + closestX <= threshold + ? { + distance: xDistances[xDistancesAbs.indexOf(closestX)], + get alignPositionIndices() { + const indices: number[] = []; + xDistancesAbs.forEach( + (val, idx) => almostEqual(val, closestX) && indices.push(idx) + ); + return indices; + }, + } + : undefined, + vert: + closestY <= threshold + ? { + distance: yDistances[yDistancesAbs.indexOf(closestY)], + get alignPositionIndices() { + const indices: number[] = []; + yDistancesAbs.forEach( + (val, idx) => almostEqual(val, closestY) && indices.push(idx) + ); + return indices; + }, + } + : undefined, }; } - private _draw() { - this._surface.refresh(); - } - - // Update X align point + /** + * Update horizontal moving distance `rst.dx` to align with other bound. + * Also, update the align points to draw. + * @param rst + * @param bound + * @param other + * @param distance + */ private _updateXAlignPoint( rst: { dx: number; dy: number }, bound: Bound, other: Bound, distance: Distance ) { - const index = distance.indexX; - rst.dx = distance.xDistance; - const alignPointX = [ + if (!distance.horiz) return; + + const { distance: dx, alignPositionIndices: distanceIndices } = + distance.horiz; + const alignXPosition = [ other.center[0], other.minX, other.maxX, - bound.minX + rst.dx, - bound.minX + rst.dx, - bound.maxX + rst.dx, - bound.maxX + rst.dx, - ][index]; - this._intraGraphicAlignLines[0] = [ - new Point(alignPointX, bound.center[1]), - new Point(alignPointX, other.center[1]), + bound.minX + dx, + bound.minX + dx, + bound.maxX + dx, + bound.maxX + dx, ]; + + rst.dx = dx; + + const dy = distance.vert?.distance ?? 0; + const top = Math.min(bound.minY + dy, other.minY); + const down = Math.max(bound.maxY + dy, other.maxY); + + this._intraGraphicAlignLines.push( + ...distanceIndices.map( + idx => + [ + new Point(alignXPosition[idx], top), + new Point(alignXPosition[idx], down), + ] as [Point, Point] + ) + ); } - // Update Y align point + /** + * Update vertical moving distance `rst.dy` to align with other bound. + * Also, update the align points to draw. + * @param rst + * @param bound + * @param other + * @param distance + */ private _updateYAlignPoint( rst: { dx: number; dy: number }, bound: Bound, other: Bound, distance: Distance ) { - const index = distance.indexY; - rst.dy = distance.yDistance; - const alignPointY = [ + if (!distance.vert) return; + + const { distance: dy, alignPositionIndices } = distance.vert; + const alignXPosition = [ other.center[1], other.minY, other.maxY, - bound.minY + rst.dy, - bound.minY + rst.dy, - bound.maxY + rst.dy, - bound.maxY + rst.dy, - ][index]; - this._intraGraphicAlignLines[1] = [ - new Point(bound.center[0], alignPointY), - new Point(other.center[0], alignPointY), + bound.minY + dy, + bound.minY + dy, + bound.maxY + dy, + bound.maxY + dy, ]; + + rst.dy = dy; + + const dx = distance.horiz?.distance ?? 0; + const left = Math.min(bound.minX + dx, other.minX); + const right = Math.max(bound.maxX + dx, other.maxX); + + this._intraGraphicAlignLines.push( + ...alignPositionIndices.map( + idx => + [ + new Point(left, alignXPosition[idx]), + new Point(right, alignXPosition[idx]), + ] as [Point, Point] + ) + ); } align(bound: Bound): { dx: number; dy: number } { const rst = { dx: 0, dy: 0 }; - const threshold = ALIGN_THRESHOLD; + const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom; const { viewport } = this.gfx; this._intraGraphicAlignLines = []; this._distributedAlignLines = []; - for (const other of this._alignableBounds) { + for (const other of this._referenceBounds.all) { const closestDistances = this._calculateClosestDistances(bound, other); - if (closestDistances.absXDistance < threshold) { + if (closestDistances.horiz) { this._updateXAlignPoint(rst, bound, other, closestDistances); } - if (closestDistances.absYDistance < threshold) { + if (closestDistances.vert) { this._updateYAlignPoint(rst, bound, other, closestDistances); } } - // point align prority is higher than distribute align + // point align priority is higher than distribute align if (rst.dx === 0) { this._alignDistributeHorizontally(rst, bound, threshold, viewport); } @@ -358,7 +587,9 @@ export class EdgelessSnapManager extends Overlay { if (rst.dy === 0) { this._alignDistributeVertically(rst, bound, threshold, viewport); } - this._draw(); + + this._renderer?.refresh(); + return rst; } @@ -369,9 +600,9 @@ export class EdgelessSnapManager extends Overlay { ) return; const { viewport } = this.gfx; - const strokeWidth = 1 / viewport.zoom; - const offset = 5 / viewport.zoom; - ctx.strokeStyle = '#1672F3'; + const strokeWidth = 2 / viewport.zoom; + + ctx.strokeStyle = '#8B5CF6'; ctx.lineWidth = strokeWidth; ctx.beginPath(); @@ -381,30 +612,31 @@ export class EdgelessSnapManager extends Overlay { const x = line[0].x; const minY = Math.min(line[0].y, line[1].y); const maxY = Math.max(line[0].y, line[1].y); - d = `M${x},${minY - offset}L${x},${maxY}`; + d = `M${x},${minY}L${x},${maxY}`; } else { const y = line[0].y; const minX = Math.min(line[0].x, line[1].x); const maxX = Math.max(line[0].x, line[1].x); - d = `M${minX - offset},${y}L${maxX + offset},${y}`; + d = `M${minX},${y}L${maxX},${y}`; } ctx.stroke(new Path2D(d)); }); + ctx.strokeStyle = '#CC4187'; this._distributedAlignLines.forEach(line => { const bar = 10 / viewport.zoom; let d = ''; if (line[0].x === line[1].x) { const x = line[0].x; - const minY = Math.min(line[0].y, line[1].y) + offset; - const maxY = Math.max(line[0].y, line[1].y) - offset; + const minY = Math.min(line[0].y, line[1].y); + const maxY = Math.max(line[0].y, line[1].y); d = `M${x},${minY}L${x},${maxY} M${x - bar},${minY}L${x + bar},${minY} M${x - bar},${maxY}L${x + bar},${maxY} `; } else { const y = line[0].y; - const minX = Math.min(line[0].x, line[1].x) + offset; - const maxX = Math.max(line[0].x, line[1].x) - offset; + const minX = Math.min(line[0].x, line[1].x); + const maxX = Math.max(line[0].x, line[1].x); d = `M${minX},${y}L${maxX},${y} M${minX},${y - bar}L${minX},${y + bar} M${maxX},${y - bar}L${maxX},${y + bar}`; @@ -413,42 +645,68 @@ export class EdgelessSnapManager extends Overlay { }); } - setupAlignables(alignables: GfxModel[], exclude: GfxModel[] = []): Bound { - if (alignables.length === 0) return new Bound(); + setMovingElements( + movingElements: GfxModel[], + excludes: GfxModel[] = [] + ): Bound { + if (movingElements.length === 0) return new Bound(); - const connectors = alignables.filter(isConnectable).reduce((prev, el) => { - const connectors = (this.gfx.surface as SurfaceBlockModel).getConnectors( - el.id - ); + const skipped = new Set(movingElements); + excludes.forEach(e => skipped.add(e)); - if (connectors.length > 0) { - prev = prev.concat(connectors); - } - - return prev; - }, [] as ConnectorElementModel[]); - - const { viewport } = this.gfx; - const viewportBounds = Bound.from(viewport.viewportBounds); - this._surface.renderer.addOverlay(this); - const canvasElements = this.gfx.layer.canvasElements; - const excludes = new Set([...alignables, ...exclude, ...connectors]); - this._alignableBounds = []; - ([...this.gfx.layer.blocks, ...canvasElements] as GfxModel[]).forEach( - alignable => { - const bounds = alignable.elementBound; - if ( - viewportBounds.isOverlapWithBound(bounds) && - !excludes.has(alignable) - ) { - this._alignableBounds.push(bounds); - } - } + const viewportBound = this.gfx.viewport.viewportBounds; + const movingBound = movingElements + .reduce( + (prev, element) => prev.unite(element.elementBound), + movingElements[0].elementBound + ) + .expand(ALIGN_THRESHOLD * this.gfx.viewport.zoom); + const horizAreaBound = new Bound( + Math.min(movingBound.x, viewportBound.x), + movingBound.y, + Math.max(movingBound.w, viewportBound.w), + movingBound.h + ); + const vertAreaBound = new Bound( + movingBound.x, + Math.min(movingBound.y, viewportBound.y), + movingBound.w, + Math.max(movingBound.h, viewportBound.h) ); - return alignables.reduce((prev, element) => { - const bounds = element.elementBound; - return prev.unite(bounds); - }, Bound.deserialize(alignables[0].xywh)); + const vertCandidates = this.gfx.grid.search(vertAreaBound, { + useSet: true, + }); + const horizCandidates = this.gfx.grid.search(horizAreaBound, { + useSet: true, + }); + const verticalBounds: Bound[] = []; + const horizBounds: Bound[] = []; + const allBounds: Bound[] = []; + + vertCandidates.forEach(candidate => { + if (skipped.has(candidate) || candidate instanceof ConnectorElementModel) + return; + verticalBounds.push(candidate.elementBound); + allBounds.push(candidate.elementBound); + }); + + horizCandidates.forEach(candidate => { + if (skipped.has(candidate) || candidate instanceof ConnectorElementModel) + return; + horizBounds.push(candidate.elementBound); + allBounds.push(candidate.elementBound); + }); + + this._referenceBounds = { + horizontal: horizBounds, + vertical: verticalBounds, + all: allBounds, + }; + + return movingElements.reduce( + (prev, element) => prev.unite(element.elementBound), + Bound.deserialize(movingElements[0].xywh) + ); } } diff --git a/blocksuite/tests-legacy/e2e/edgeless/lock.spec.ts b/blocksuite/tests-legacy/e2e/edgeless/lock.spec.ts index d35c00c4e1..fccb6040d2 100644 --- a/blocksuite/tests-legacy/e2e/edgeless/lock.spec.ts +++ b/blocksuite/tests-legacy/e2e/edgeless/lock.spec.ts @@ -194,16 +194,16 @@ test.describe('lock', () => { const { lock, unlock } = getButtons(page); await lock.click(); - await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]); // frame outline and shape + await assertEdgelessSelectedModelRect(page, [0, 0, 125, 125]); // frame outline and shape await pressEscape(page); await clickView(page, [100, 100]); - await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]); + await assertEdgelessSelectedModelRect(page, [0, 0, 125, 125]); await unlock.click(); await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); await pressEscape(page); await clickView(page, [100, 100]); - await assertEdgelessSelectedModelRect(page, [70, 70, 50, 50]); + await assertEdgelessSelectedModelRect(page, [75, 75, 50, 50]); await deleteAll(page); diff --git a/blocksuite/tests-legacy/e2e/edgeless/selection/selection.spec.ts b/blocksuite/tests-legacy/e2e/edgeless/selection/selection.spec.ts index 754838e5f4..220988c4e2 100644 --- a/blocksuite/tests-legacy/e2e/edgeless/selection/selection.spec.ts +++ b/blocksuite/tests-legacy/e2e/edgeless/selection/selection.spec.ts @@ -170,7 +170,7 @@ test('selection box of shape element sync on fast dragging', async ({ { click: true } ); - await assertEdgelessSelectedRect(page, [650, 446, 100, 100]); + await assertEdgelessSelectedRect(page, [650, 450, 100, 100]); }); test('when the selection is always a note, it should remain in an active state', async ({ diff --git a/blocksuite/tests-legacy/e2e/edgeless/snap.spec.ts b/blocksuite/tests-legacy/e2e/edgeless/snap.spec.ts index 666e54f76e..acceca5754 100644 --- a/blocksuite/tests-legacy/e2e/edgeless/snap.spec.ts +++ b/blocksuite/tests-legacy/e2e/edgeless/snap.spec.ts @@ -18,11 +18,11 @@ test.describe('snap', () => { await assertSelectedBound(page, [300, 0, 100, 100]); - await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 5]); - await assertSelectedBound(page, [300, 5, 100, 100]); + await dragBetweenViewCoords(page, [350, 50], [350, 50 + 10]); + await assertSelectedBound(page, [300, 10, 100, 100]); await undoByClick(page); - await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 3]); + await dragBetweenViewCoords(page, [350, 50], [350, 50 + 7]); await assertSelectedBound(page, [300, 0, 100, 100]); });