UNPKG

jjb-lc-designable

Version:

基于alibaba-designable源码二次封装的表单设计器。

438 lines (434 loc) 15.9 kB
import { Point, calcEdgeLinesOfRect, calcBoundingRect, calcSpaceBlockOfRect, calcElementTranslate, calcDistanceOfSnapLineToEdges, Rect, isEqualRect, isLineSegment, calcClosestEdges, calcCombineSnapLineSegment } from 'jjb-lc-designable/shared'; import { observable, define, action } from 'jjb-lc-formily/reactive'; import { SpaceBlock } from './SpaceBlock'; import { TreeNode } from './TreeNode'; import { SnapLine } from './SnapLine'; import { CursorDragType } from './Cursor'; export class TransformHelper { dragNodes = []; rulerSnapLines = []; aroundSnapLines = []; aroundSpaceBlocks = null; viewportRectsStore = {}; dragStartTranslateStore = {}; dragStartSizeStore = {}; dragStartNodesRect = null; snapping = false; dragging = false; snapped = false; constructor(props) { this.operation = props.operation; this.makeObservable(); } get tree() { return this.operation.tree; } get cursor() { return this.operation.engine.cursor; } get viewport() { return this.operation.workspace.viewport; } get deltaX() { return this.cursor.dragStartToCurrentDelta.clientX; } get deltaY() { return this.cursor.dragStartToCurrentDelta.clientY; } get cursorPosition() { const position = this.cursor.position; return this.operation.workspace.viewport.getOffsetPoint(new Point(position.clientX, position.clientY)); } get cursorDragNodesRect() { if (this.type === 'translate') { return new Rect(this.cursorPosition.x - this.dragStartCursorOffset.x, this.cursorPosition.y - this.dragStartCursorOffset.y, this.dragNodesRect.width, this.dragNodesRect.height); } else if (this.type === 'resize') { const dragNodesRect = this.dragStartNodesRect; const deltaX = this.cursor.dragStartToCurrentDelta.clientX; const deltaY = this.cursor.dragStartToCurrentDelta.clientY; switch (this.direction) { case 'left-top': return new Rect(this.cursorPosition.x - this.dragStartCursorOffset.x, this.cursorPosition.y - this.dragStartCursorOffset.y, dragNodesRect.width - deltaX, dragNodesRect.height - deltaY); case 'left-center': return new Rect(this.cursorPosition.x - this.dragStartCursorOffset.x, dragNodesRect.y, dragNodesRect.width - deltaX, dragNodesRect.height); case 'left-bottom': return new Rect(this.cursorPosition.x - this.dragStartCursorOffset.x, dragNodesRect.y, dragNodesRect.width - deltaX, dragNodesRect.height - deltaY); case 'center-top': return new Rect(dragNodesRect.x, this.cursorPosition.y - this.dragStartCursorOffset.y, dragNodesRect.width, dragNodesRect.height - deltaY); case 'center-bottom': return new Rect(dragNodesRect.x, dragNodesRect.y, dragNodesRect.width, dragNodesRect.height + deltaY); case 'right-top': return new Rect(dragNodesRect.x, this.cursorPosition.y - this.dragStartCursorOffset.y, dragNodesRect.width + deltaX, dragNodesRect.height - deltaY); case 'right-center': return new Rect(dragNodesRect.x, dragNodesRect.y, dragNodesRect.width + deltaX, dragNodesRect.height); case 'right-bottom': return new Rect(dragNodesRect.x, dragNodesRect.y, dragNodesRect.width + deltaX, dragNodesRect.height - deltaY); } } } get cursorDragNodesEdgeLines() { return calcEdgeLinesOfRect(this.cursorDragNodesRect); } get dragNodesRect() { if (this.draggingNodesRect) return this.draggingNodesRect; return calcBoundingRect(this.dragNodes.map(node => node.getValidElementOffsetRect())); } get dragNodesEdgeLines() { return calcEdgeLinesOfRect(this.dragNodesRect); } get cursorOffset() { return new Point(this.cursorPosition.x - this.dragNodesRect.x, this.cursorPosition.y - this.dragNodesRect.y); } get dragStartCursor() { const position = this.operation.engine.cursor.dragStartPosition; return this.operation.workspace.viewport.getOffsetPoint(new Point(position.clientX, position.clientY)); } get dragStartCursorOffset() { return new Point(this.dragStartCursor.x - this.dragStartNodesRect.x, this.dragStartCursor.y - this.dragStartNodesRect.y); } get closestSnapLines() { if (!this.dragging) return []; const results = []; const cursorDragNodesEdgeLines = this.cursorDragNodesEdgeLines; this.thresholdSnapLines.forEach(line => { const distance = calcDistanceOfSnapLineToEdges(line, cursorDragNodesEdgeLines); if (distance < TransformHelper.threshold) { const existed = results.findIndex(l => l.distance > distance && l.distance > 0 && l.direction === line.direction); if (existed > -1) { results.splice(existed, 1); } results.push(line); } }); return results; } get closestSpaceBlocks() { if (!this.dragging) return []; const cursorDragNodesEdgeLines = this.cursorDragNodesEdgeLines; return this.thresholdSpaceBlocks.filter(block => { const line = block.snapLine; if (!line) return false; return calcDistanceOfSnapLineToEdges(line, cursorDragNodesEdgeLines) < TransformHelper.threshold; }); } get thresholdSnapLines() { if (!this.dragging) return []; const lines = []; this.aroundSnapLines.forEach(line => { lines.push(line); }); this.rulerSnapLines.forEach(line => { if (line.closest) { lines.push(line); } }); for (let type in this.aroundSpaceBlocks) { const block = this.aroundSpaceBlocks[type]; const line = block.snapLine; if (line) { lines.push(line); } } return lines; } get thresholdSpaceBlocks() { const results = []; if (!this.dragging) return []; for (let type in this.aroundSpaceBlocks) { const block = this.aroundSpaceBlocks[type]; if (!block.snapLine) return []; if (block.snapLine.distance !== 0) return []; if (block.isometrics.length) { results.push(block); results.push(...block.isometrics); } } return results; } get measurerSpaceBlocks() { const results = []; if (!this.dragging || !this.snapped) return []; for (let type in this.aroundSpaceBlocks) { if (this.aroundSpaceBlocks[type]) results.push(this.aroundSpaceBlocks[type]); } return results; } calcBaseTranslate(node) { const dragStartTranslate = this.dragStartTranslateStore[node.id] ?? { x: 0, y: 0 }; const x = dragStartTranslate.x + this.deltaX, y = dragStartTranslate.y + this.deltaY; return { x, y }; } calcBaseResize(node) { const deltaX = this.deltaX; const deltaY = this.deltaY; const dragStartTranslate = this.dragStartTranslateStore[node.id] ?? { x: 0, y: 0 }; const dragStartSize = this.dragStartSizeStore[node.id] ?? { width: 0, height: 0 }; switch (this.direction) { case 'left-top': return new Rect(dragStartTranslate.x + deltaX, dragStartTranslate.y + deltaY, dragStartSize.width - deltaX, dragStartSize.height - deltaY); case 'left-center': return new Rect(dragStartTranslate.x + deltaX, dragStartTranslate.y, dragStartSize.width - deltaX, dragStartSize.height); case 'left-bottom': return new Rect(dragStartTranslate.x + deltaX, dragStartTranslate.y, dragStartSize.width - deltaX, dragStartSize.height + deltaY); case 'center-bottom': return new Rect(dragStartTranslate.x, dragStartTranslate.y, dragStartSize.width, dragStartSize.height + deltaY); case 'center-top': return new Rect(dragStartTranslate.x, dragStartTranslate.y + deltaY, dragStartSize.width, dragStartSize.height - deltaY); case 'right-top': return new Rect(dragStartTranslate.x, dragStartTranslate.y + deltaY, dragStartSize.width + deltaX, dragStartSize.height - deltaY); case 'right-bottom': return new Rect(dragStartTranslate.x, dragStartTranslate.y, dragStartSize.width + deltaX, dragStartSize.height + deltaY); case 'right-center': return new Rect(dragStartTranslate.x, dragStartTranslate.y, dragStartSize.width + deltaX, dragStartSize.height); } } calcDragStartStore(nodes = []) { this.dragStartNodesRect = this.dragNodesRect; nodes.forEach(node => { const element = node.getElement(); const rect = node.getElementOffsetRect(); this.dragStartTranslateStore[node.id] = calcElementTranslate(element); this.dragStartSizeStore[node.id] = { width: rect.width, height: rect.height }; }); } calcRulerSnapLines(dragNodesRect) { const edgeLines = calcEdgeLinesOfRect(dragNodesRect); return this.rulerSnapLines.map(line => { line.distance = calcDistanceOfSnapLineToEdges(line, edgeLines); return line; }); } calcAroundSnapLines(dragNodesRect) { const results = []; const edgeLines = calcEdgeLinesOfRect(dragNodesRect); this.eachViewportNodes((refer, referRect) => { if (this.dragNodes.includes(refer)) return; const referLines = calcEdgeLinesOfRect(referRect); const add = line => { const [distance, edge] = calcClosestEdges(line, edgeLines); const combined = calcCombineSnapLineSegment(line, edge); if (distance < TransformHelper.threshold) { if (this.snapping && distance !== 0) return; const snapLine = new SnapLine(this, { ...combined, distance }); const edge = snapLine.snapEdge(dragNodesRect); if (this.type === 'translate') { results.push(snapLine); } else if (edge !== 'hc' && edge !== 'vc') { results.push(snapLine); } } }; referLines.h.forEach(add); referLines.v.forEach(add); }); return results; } calcAroundSpaceBlocks(dragNodesRect) { const closestSpaces = {}; this.eachViewportNodes((refer, referRect) => { if (isEqualRect(dragNodesRect, referRect)) return; const origin = calcSpaceBlockOfRect(dragNodesRect, referRect); if (origin) { const spaceBlock = new SpaceBlock(this, { refer, ...origin }); if (!closestSpaces[origin.type]) { closestSpaces[origin.type] = spaceBlock; } else if (spaceBlock.distance < closestSpaces[origin.type].distance) { closestSpaces[origin.type] = spaceBlock; } } }); return closestSpaces; } calcViewportNodes() { this.tree.eachTree(node => { const topRect = node.getValidElementRect(); const offsetRect = node.getValidElementOffsetRect(); if (this.dragNodes.includes(node)) return; if (this.viewport.isRectInViewport(topRect)) { this.viewportRectsStore[node.id] = offsetRect; } }); } getNodeRect(node) { return this.viewportRectsStore[node.id]; } eachViewportNodes(visitor) { for (let id in this.viewportRectsStore) { visitor(this.tree.findById(id), this.viewportRectsStore[id]); } } translate(node, handler) { if (!this.dragging) return; const translate = this.calcBaseTranslate(node); this.snapped = false; this.snapping = false; for (let line of this.closestSnapLines) { line.translate(node, translate); this.snapping = true; this.snapped = true; } handler(translate); if (this.snapping) { this.dragMove(); this.snapping = false; } } resize(node, handler) { if (!this.dragging) return; const rect = this.calcBaseResize(node); this.snapping = false; this.snapping = false; for (let line of this.closestSnapLines) { line.resize(node, rect); this.snapping = true; this.snapped = true; } handler(rect); if (this.snapping) { this.dragMove(); this.snapping = false; } } // rotate(node: TreeNode, handler: (rotate: number) => void) {} // scale(node: TreeNode, handler: (scale: number) => void) {} // round(node: TreeNode, handler: (round: number) => void) {} findRulerSnapLine(id) { return this.rulerSnapLines.find(item => item.id === id); } addRulerSnapLine(line) { if (!isLineSegment(line)) return; if (!this.findRulerSnapLine(line.id)) { this.rulerSnapLines.push(new SnapLine(this, { ...line, type: 'ruler' })); } } removeRulerSnapLine(id) { const matchedLineIndex = this.rulerSnapLines.findIndex(item => item.id === id); if (matchedLineIndex > -1) { this.rulerSnapLines.splice(matchedLineIndex, 1); } } dragStart(props) { const dragNodes = props?.dragNodes; const type = props?.type; const direction = props?.direction; if (type === 'resize') { const nodes = TreeNode.filterResizable(dragNodes); if (nodes.length) { this.dragging = true; this.type = type; this.direction = direction; this.dragNodes = nodes; this.calcDragStartStore(nodes); this.cursor.setDragType(CursorDragType.Resize); } } else if (type === 'translate') { const nodes = TreeNode.filterTranslatable(dragNodes); if (nodes.length) { this.dragging = true; this.type = type; this.direction = direction; this.dragNodes = nodes; this.calcDragStartStore(nodes); this.cursor.setDragType(CursorDragType.Translate); } } else if (type === 'rotate') { const nodes = TreeNode.filterRotatable(dragNodes); if (nodes.length) { this.dragging = true; this.type = type; this.dragNodes = nodes; this.calcDragStartStore(nodes); this.cursor.setDragType(CursorDragType.Rotate); } } else if (type === 'scale') { const nodes = TreeNode.filterScalable(dragNodes); if (nodes.length) { this.dragging = true; this.type = type; this.dragNodes = nodes; this.calcDragStartStore(nodes); this.cursor.setDragType(CursorDragType.Scale); } } else if (type === 'round') { const nodes = TreeNode.filterRoundable(dragNodes); if (nodes.length) { this.dragging = true; this.type = type; this.dragNodes = nodes; this.calcDragStartStore(nodes); this.cursor.setDragType(CursorDragType.Round); } } if (this.dragging) { this.calcViewportNodes(); } } dragMove() { if (!this.dragging) return; this.draggingNodesRect = null; this.draggingNodesRect = this.dragNodesRect; this.rulerSnapLines = this.calcRulerSnapLines(this.dragNodesRect); this.aroundSnapLines = this.calcAroundSnapLines(this.dragNodesRect); this.aroundSpaceBlocks = this.calcAroundSpaceBlocks(this.dragNodesRect); } dragEnd() { this.dragging = false; this.viewportRectsStore = {}; this.dragStartTranslateStore = {}; this.aroundSnapLines = []; this.draggingNodesRect = null; this.aroundSpaceBlocks = null; this.dragStartNodesRect = null; this.dragNodes = []; this.cursor.setDragType(CursorDragType.Move); } makeObservable() { define(this, { snapped: observable.ref, dragging: observable.ref, snapping: observable.ref, dragNodes: observable.ref, aroundSnapLines: observable.ref, aroundSpaceBlocks: observable.ref, rulerSnapLines: observable.shallow, closestSnapLines: observable.computed, thresholdSnapLines: observable.computed, thresholdSpaceBlocks: observable.computed, measurerSpaceBlocks: observable.computed, cursor: observable.computed, cursorPosition: observable.computed, cursorOffset: observable.computed, dragStartCursor: observable.computed, translate: action, dragStart: action, dragMove: action, dragEnd: action }); } static threshold = 6; }