diff --git a/__tests__/graph/graph.spec.ts b/__tests__/graph/graph.spec.ts index 3ab0c856f64..4b95faa88d8 100644 --- a/__tests__/graph/graph.spec.ts +++ b/__tests__/graph/graph.spec.ts @@ -725,6 +725,44 @@ describe('Graph: Grid / 网格', () => { cleanup() }) + it('snapToGrid: 支持通过 grid.snapToGrid 关闭自动吸附', () => { + const { graph, cleanup } = createTestGraph({ + grid: { size: 10, snapToGrid: false }, + }) + + expect(graph.snapToGrid(12, 18)).toEqual({ x: 12, y: 18 }) + + cleanup() + }) + + it('snapToGrid: grid 为 false 时默认关闭自动吸附', () => { + const { graph, cleanup } = createTestGraph({ + grid: false, + }) + + expect(graph.snapToGrid(12, 18)).toEqual({ x: 12, y: 18 }) + + cleanup() + }) + + it('drawGrid: 支持动态切换 snapToGrid', () => { + const { graph, cleanup } = createTestGraph({ + grid: { size: 10, visible: true }, + }) + + expect(graph.snapToGrid(12, 18)).toEqual({ x: 10, y: 20 }) + + graph.drawGrid({ snapToGrid: false }) + expect(graph.options.grid.snapToGrid).toBe(false) + expect(graph.snapToGrid(12, 18)).toEqual({ x: 12, y: 18 }) + + graph.drawGrid({ snapToGrid: true }) + expect(graph.options.grid.snapToGrid).toBe(true) + expect(graph.snapToGrid(12, 18)).toEqual({ x: 10, y: 20 }) + + cleanup() + }) + it('showGrid / hideGrid / drawGrid: 网格显示控制', () => { const { graph, cleanup } = createTestGraph() diff --git a/__tests__/plugin/dnd.spec.ts b/__tests__/plugin/dnd.spec.ts index ef260d761a5..941fbaf1cf8 100644 --- a/__tests__/plugin/dnd.spec.ts +++ b/__tests__/plugin/dnd.spec.ts @@ -272,6 +272,58 @@ describe('Dnd', () => { expect((result as Node).position()).toEqual({ x: 450, y: 350 }) }) + it('should snap dropped node position when grid.snapToGrid is true', () => { + const node = new Rect({ + x: 100, + y: 100, + width: 100, + height: 100, + }) + + graph.options.grid.snapToGrid = true + const result = dnd['drop'](node, { + x: 401, + y: 303, + }) + + expect((result as Node).position()).toEqual({ x: 450, y: 350 }) + }) + + it('should keep snapline alignment when snapOffset is set', () => { + const node = new Rect({ + x: 100, + y: 100, + width: 100, + height: 100, + }) + + graph.options.grid.snapToGrid = true + dnd['snapOffset'] = { x: 7, y: -8 } + const result = dnd['drop'](node, { + x: 401, + y: 303, + }) + + expect((result as Node).position()).toEqual({ x: 451, y: 353 }) + }) + + it('should not snap dropped node position when grid.snapToGrid is false', () => { + const node = new Rect({ + x: 100, + y: 100, + width: 100, + height: 100, + }) + + graph.options.grid.snapToGrid = false + const result = dnd['drop'](node, { + x: 401, + y: 303, + }) + + expect((result as Node).position()).toEqual({ x: 451, y: 353 }) + }) + it('should return null if is not inside valid area', () => { const node = new Rect({ width: 100, diff --git a/__tests__/plugin/transform.spec.ts b/__tests__/plugin/transform.spec.ts index 8474d78d5a4..2d384b50f4d 100644 --- a/__tests__/plugin/transform.spec.ts +++ b/__tests__/plugin/transform.spec.ts @@ -12,6 +12,9 @@ function createMockGraph() { off: vi.fn(), trigger: vi.fn(), getPlugin: vi.fn(), + grid: { + isSnapToGridEnabled: () => true, + }, snapToGrid: (x: number, y: number) => ({ x, y }), getGridSize: () => 1, findViewByCell: (node: any) => ({ diff --git a/__tests__/view/node/index.spec.ts b/__tests__/view/node/index.spec.ts index 61c06ba14b9..e0564a49fc9 100644 --- a/__tests__/view/node/index.spec.ts +++ b/__tests__/view/node/index.spec.ts @@ -751,6 +751,54 @@ describe('NodeView', () => { ) expect(node.setPosition).toHaveBeenCalled() }) + + it('should snap node position when grid.snapToGrid is true', () => { + const event = { clientX: 153, clientY: 257 } as any + const data = { + moving: true, + offset: { x: -50, y: -50 }, + restrict: null, + } + + graph.options.grid.snapToGrid = true + vi.spyOn(nodeView, 'getEventData').mockReturnValue(data) + vi.spyOn(nodeView as any, 'autoScrollGraph').mockImplementation(() => {}) + vi.spyOn(graph, 'getGridSize').mockReturnValue(10) + vi.spyOn(node, 'setPosition').mockImplementation(() => {}) + vi.spyOn(graph.options.embedding, 'enabled', 'get').mockReturnValue(false) + + ;(nodeView as any).dragNode(event, 153, 257) + + expect(node.setPosition).toHaveBeenCalledWith(100, 210, { + restrict: null, + deep: true, + ui: true, + }) + }) + + it('should not snap node position when grid.snapToGrid is false', () => { + const event = { clientX: 153, clientY: 257 } as any + const data = { + moving: true, + offset: { x: -50, y: -50 }, + restrict: null, + } + + graph.options.grid.snapToGrid = false + vi.spyOn(nodeView, 'getEventData').mockReturnValue(data) + vi.spyOn(nodeView as any, 'autoScrollGraph').mockImplementation(() => {}) + vi.spyOn(graph, 'getGridSize').mockReturnValue(10) + vi.spyOn(node, 'setPosition').mockImplementation(() => {}) + vi.spyOn(graph.options.embedding, 'enabled', 'get').mockReturnValue(false) + + ;(nodeView as any).dragNode(event, 153, 257) + + expect(node.setPosition).toHaveBeenCalledWith(103, 207, { + restrict: null, + deep: true, + ui: true, + }) + }) }) describe('additional methods', () => { diff --git a/src/graph/coord.ts b/src/graph/coord.ts index 5d820a6c7bd..fd308e36891 100644 --- a/src/graph/coord.ts +++ b/src/graph/coord.ts @@ -29,7 +29,9 @@ export class CoordManager extends Base { typeof x === 'number' ? this.clientToLocalPoint(x, y as number) : this.clientToLocalPoint(x.x, x.y) - return p.snapToGrid(this.graph.getGridSize()) + return this.graph.grid.isSnapToGridEnabled() + ? p.snapToGrid(this.graph.getGridSize()) + : p } localToGraphPoint(x: number | Point | PointLike, y?: number) { diff --git a/src/graph/graph.ts b/src/graph/graph.ts index c82b7a36191..ef555e99ef3 100644 --- a/src/graph/graph.ts +++ b/src/graph/graph.ts @@ -54,7 +54,11 @@ import { CSSManager as Css } from './css' import { DefsManager as Defs } from './defs' import type { FilterOptions, GradientOptions, MarkerOptions } from './defs' import type { EventArgs } from './events' -import { GridManager as Grid, GridDrawOptions } from './grid' +import { + GridManager as Grid, + type GridCommonOptions, + type GridDrawOptions, +} from './grid' import { HighlightManager as Highlight } from './highlight' import { MouseWheel as Wheel } from './mousewheel' import { GraphDefinition, GraphManual, getOptions } from './options' @@ -1177,7 +1181,7 @@ export class Graph extends Basecoat { return this } - drawGrid(options?: GridDrawOptions) { + drawGrid(options?: Graph.GridManager.Options) { this.grid.draw(options) return this } @@ -1421,3 +1425,9 @@ export class Graph extends Basecoat { // #endregion } + +export namespace Graph { + export namespace GridManager { + export type Options = Partial & GridDrawOptions + } +} diff --git a/src/graph/grid.ts b/src/graph/grid.ts index e7ad6d7b082..73a35b48d2a 100644 --- a/src/graph/grid.ts +++ b/src/graph/grid.ts @@ -47,6 +47,10 @@ export class GridManager extends Base { return this.grid.size } + isSnapToGridEnabled() { + return this.grid.snapToGrid + } + setGridSize(size: number) { this.grid.size = Math.max(size, 1) this.update() @@ -66,7 +70,7 @@ export class GridManager extends Base { this.elem.style.backgroundImage = '' } - draw(options?: GridDrawOptions) { + draw(options?: GridManager.Options) { this.clear() this.instance = null Object.assign(this.grid, options) @@ -200,6 +204,11 @@ export type GridDrawOptions = export interface GridCommonOptions { size: number visible: boolean + snapToGrid: boolean } export type GridOptions = GridCommonOptions & GridDrawOptions + +export namespace GridManager { + export type Options = Partial & GridDrawOptions +} diff --git a/src/graph/options.ts b/src/graph/options.ts index 08526450f81..0a66af9079c 100644 --- a/src/graph/options.ts +++ b/src/graph/options.ts @@ -340,11 +340,15 @@ export function getOptions(options: Partial) { // grid // ---- - const defaultGrid: GridCommonOptions = { size: 10, visible: false } + const defaultGrid: GridCommonOptions = { + size: 10, + visible: false, + snapToGrid: true, + } if (typeof grid === 'number') { - result.grid = { size: grid, visible: false } + result.grid = { size: grid, visible: false, snapToGrid: true } } else if (typeof grid === 'boolean') { - result.grid = { ...defaultGrid, visible: grid } + result.grid = { ...defaultGrid, visible: grid, snapToGrid: grid } } else { result.grid = { ...defaultGrid, ...grid } } @@ -400,6 +404,7 @@ export const defaults: Partial = { grid: { size: 10, visible: false, + snapToGrid: true, }, background: false, diff --git a/src/plugin/dnd/index.ts b/src/plugin/dnd/index.ts index 175cc332841..8734b67c334 100644 --- a/src/plugin/dnd/index.ts +++ b/src/plugin/dnd/index.ts @@ -475,7 +475,9 @@ export class Dnd extends View implements GraphPlugin { const bbox = droppingNode.getBBox() local.x += bbox.x - bbox.width / 2 local.y += bbox.y - bbox.height / 2 - const gridSize = this.snapOffset ? 1 : targetGraph.getGridSize() + const shouldSnapToGrid = + !this.snapOffset && targetGraph.grid.isSnapToGridEnabled() + const gridSize = shouldSnapToGrid ? targetGraph.getGridSize() : 1 droppingNode.position( snapToGrid(local.x, gridSize), diff --git a/src/plugin/transform/transform.ts b/src/plugin/transform/transform.ts index 638507e8d6c..643fd9bf714 100644 --- a/src/plugin/transform/transform.ts +++ b/src/plugin/transform/transform.ts @@ -418,8 +418,10 @@ export class TransformImpl extends View { const rawWidth = width const rawHeight = height - width = snapToGrid(width, gridSize) - height = snapToGrid(height, gridSize) + if (this.graph.grid.isSnapToGridEnabled()) { + width = snapToGrid(width, gridSize) + height = snapToGrid(height, gridSize) + } width = Math.max(width, options.minWidth || gridSize) height = Math.max(height, options.minHeight || gridSize) width = Math.min(width, options.maxWidth || Infinity) diff --git a/src/view/node/index.ts b/src/view/node/index.ts index e0211894c7f..f3c2fb6272d 100644 --- a/src/view/node/index.ts +++ b/src/view/node/index.ts @@ -1179,8 +1179,14 @@ export class NodeView< this.autoScrollGraph(e.clientX, e.clientY) - const posX = snapToGrid(x + offset.x, gridSize) - const posY = snapToGrid(y + offset.y, gridSize) + const rawX = x + offset.x + const rawY = y + offset.y + const posX = graph.grid.isSnapToGridEnabled() + ? snapToGrid(rawX, gridSize) + : rawX + const posY = graph.grid.isSnapToGridEnabled() + ? snapToGrid(rawY, gridSize) + : rawY node.setPosition(posX, posY, { restrict, deep: true,