diff --git a/src/app/core/models/cursor.ts b/src/app/core/models/cursor.ts new file mode 100644 index 0000000..0d3f928 --- /dev/null +++ b/src/app/core/models/cursor.ts @@ -0,0 +1,116 @@ +import { Engine } from './engine'; +import { isValidNumber } from '../../shared/types'; +import { globalThisPolyfill } from '../../shared/globalThisPolyfill'; + +export enum CursorStatus { + Normal = 'NORMAL', + DragStart = 'DRAG_START', + Dragging = 'DRAGGING', + DragStop = 'DRAG_STOP' +} + +export enum CursorDragType { + Move = 'MOVE', + Resize = 'RESIZE', + Rotate = 'ROTATE', + Scale = 'SCALE', + Translate = 'TRANSLATE', + Round = 'ROUND' +} + +export enum CursorType { + Normal = 'NORMAL', + Selection = 'SELECTION', + Sketch = 'SKETCH' +} + +export interface ICursorPosition { + pageX?: number; + + pageY?: number; + + clientX?: number; + + clientY?: number; + + topPageX?: number; + + topPageY?: number; + + topClientX?: number; + + topClientY?: number; +} + +export interface ICursor { + status?: CursorStatus; + + position?: ICursorPosition; + + dragStartPosition?: ICursorPosition; + + dragEndPosition?: ICursorPosition; + + view?: Window; +} + +const DEFAULT_POSITION = { + pageX: 0, + pageY: 0, + clientX: 0, + clientY: 0, + topPageX: 0, + topPageY: 0, + topClientX: 0, + topClientY: 0 +}; + +const setCursorStyle = (contentWindow: Window, style: string) => { + const currentRoot = document?.getElementsByTagName?.('html')?.[0]; + const root = contentWindow?.document?.getElementsByTagName('html')?.[0]; + if (root && root.style.cursor !== style) { + root.style.cursor = style; + } + if (currentRoot && currentRoot.style.cursor !== style) { + currentRoot.style.cursor = style; + } +}; + +const calcPositionDelta = (end: ICursorPosition, start: ICursorPosition): ICursorPosition => { + return Object.keys(end || {}).reduce((buf, key) => { + if (isValidNumber(end?.[key]) && isValidNumber(start?.[key])) { + buf[key] = end[key] - start[key]; + } else { + buf[key] = end[key]; + } + return buf; + }, {}); +}; + +export class Cursor { + engine: Engine; + + type: CursorType | string = CursorType.Normal; + + dragType: CursorDragType | string = CursorDragType.Move; + + status: CursorStatus = CursorStatus.Normal; + + position: ICursorPosition = DEFAULT_POSITION; + + dragStartPosition: ICursorPosition; + + dragEndPosition: ICursorPosition; + + dragAtomDelta: ICursorPosition = DEFAULT_POSITION; + + dragStartToCurrentDelta: ICursorPosition = DEFAULT_POSITION; + + dragStartToEndDelta: ICursorPosition = DEFAULT_POSITION; + + view: Window = globalThisPolyfill; + + constructor(engine: Engine) { + this.engine = engine; + } +} diff --git a/src/app/core/models/engine.ts b/src/app/core/models/engine.ts index 2cf97e0..5fd85ab 100644 --- a/src/app/core/models/engine.ts +++ b/src/app/core/models/engine.ts @@ -1,3 +1,31 @@ +import { Workbench } from '@/app/core/models/workbench'; +import { IEngineProps } from '@/app/shared/types'; +import { Cursor } from '@/app/core/models/cursor'; +import { Screen } from '@/app/core/models/screen'; +import { uid } from '@/app/shared/uid'; + export class Engine { - props: any; + id: string; + + props: IEngineProps; + + cursor: Cursor; + + workbench: Workbench; + + // keyboard: Keyboard + + screen: Screen; + + constructor() { + this.init(); + this.id = uid(); + } + + init() { + this.workbench = new Workbench(this); + this.screen = new Screen(this); + this.cursor = new Cursor(this); + // this.keyboard = new Keyboard(this) + } } diff --git a/src/app/core/models/history.ts b/src/app/core/models/history.ts new file mode 100644 index 0000000..d35a4e3 --- /dev/null +++ b/src/app/core/models/history.ts @@ -0,0 +1,27 @@ +export interface IHistoryProps { + onPush?: (item: T) => void; + onRedo?: (item: T) => void; + onUndo?: (item: T) => void; + onGoto?: (item: T) => void; +} + +export interface HistoryItem { + data: T; + type?: string; + timestamp: number; +} + +export interface ISerializable { + from(json: any): void; //导入数据 + serialize(): any; //序列化模型,用于历史记录保存 +} + +export class History { + context: ISerializable; + props: IHistoryProps>; + current = 0; + history: HistoryItem[] = []; + updateTimer = null; + maxSize = 100; + locking = false; +} diff --git a/src/app/core/models/hover.ts b/src/app/core/models/hover.ts new file mode 100644 index 0000000..ede9c2f --- /dev/null +++ b/src/app/core/models/hover.ts @@ -0,0 +1,7 @@ +import { Operation } from './operation'; +import { TreeNode } from './tree-node'; + +export class Hover { + node: TreeNode = null; + operation: Operation; +} diff --git a/src/app/core/models/move-helper.ts b/src/app/core/models/move-helper.ts new file mode 100644 index 0000000..1db2c86 --- /dev/null +++ b/src/app/core/models/move-helper.ts @@ -0,0 +1,66 @@ +import { Operation } from './operation'; +import { TreeNode } from './tree-node'; +import { Viewport } from './viewport'; +import { IPoint, Rect } from '../../shared/coordinate'; + +export enum ClosestPosition { + Before = 'BEFORE', + ForbidBefore = 'FORBID_BEFORE', + After = 'After', + ForbidAfter = 'FORBID_AFTER', + Upper = 'UPPER', + ForbidUpper = 'FORBID_UPPER', + Under = 'UNDER', + ForbidUnder = 'FORBID_UNDER', + Inner = 'INNER', + ForbidInner = 'FORBID_INNER', + InnerAfter = 'INNER_AFTER', + ForbidInnerAfter = 'FORBID_INNER_AFTER', + InnerBefore = 'INNER_BEFORE', + ForbidInnerBefore = 'FORBID_INNER_BEFORE', + Forbid = 'FORBID' +} + +export interface IMoveHelperProps { + operation: Operation; +} + +export interface IMoveHelperDragStartProps { + dragNodes: TreeNode[]; +} + +export interface IMoveHelperDragDropProps { + dropNode: TreeNode; +} +export interface IMoveHelperDragMoveProps { + touchNode: TreeNode; + point: IPoint; +} + +export class MoveHelper { + operation: Operation; + + rootNode: TreeNode; + + dragNodes: TreeNode[] = []; + + touchNode: TreeNode = null; + + closestNode: TreeNode = null; + + activeViewport: Viewport = null; + + viewportClosestRect: Rect = null; + + outlineClosestRect: Rect = null; + + viewportClosestOffsetRect: Rect = null; + + outlineClosestOffsetRect: Rect = null; + + viewportClosestDirection: ClosestPosition = null; + + outlineClosestDirection: ClosestPosition = null; + + dragging = false; +} diff --git a/src/app/core/models/operation.ts b/src/app/core/models/operation.ts new file mode 100644 index 0000000..635fa58 --- /dev/null +++ b/src/app/core/models/operation.ts @@ -0,0 +1,32 @@ +import { ITreeNode, TreeNode } from './tree-node'; +import { Engine } from './engine'; +import { Workspace } from './workspace'; +import { Selection } from './selection'; +import { Hover } from './hover'; +import { TransformHelper } from './transform-helper'; +import { MoveHelper } from './move-helper'; + +export interface IOperation { + tree?: ITreeNode; + selected?: string[]; +} + +export class Operation { + workspace: Workspace; + + engine: Engine; + + tree: TreeNode; + + selection: Selection; + + hover: Hover; + + transformHelper: TransformHelper; + + moveHelper: MoveHelper; + + requests = { + snapshot: null + }; +} diff --git a/src/app/core/models/selection.ts b/src/app/core/models/selection.ts new file mode 100644 index 0000000..29b58f5 --- /dev/null +++ b/src/app/core/models/selection.ts @@ -0,0 +1,11 @@ +import { Operation } from './operation'; + +export interface ISelection { + selected?: string[]; + operation?: Operation; +} +export class Selection { + operation: Operation; + selected: string[] = []; + indexes: Record = {}; +} diff --git a/src/app/core/models/snapline.ts b/src/app/core/models/snapline.ts new file mode 100644 index 0000000..0d8845d --- /dev/null +++ b/src/app/core/models/snapline.ts @@ -0,0 +1,20 @@ +import { TransformHelper } from './transform-helper'; +import { TreeNode } from './tree-node'; +import { ILineSegment, IPoint } from '../../shared/coordinate'; +export type ISnapLineType = 'ruler' | 'space-block' | 'normal'; + +export type ISnapLine = ILineSegment & { + type?: ISnapLineType; + distance?: number; + id?: string; + refer?: TreeNode; +}; +export class SnapLine { + _id: string; + type: ISnapLineType; + distance: number; + refer: TreeNode; + start: IPoint; + end: IPoint; + helper: TransformHelper; +} diff --git a/src/app/core/models/space-block.ts b/src/app/core/models/space-block.ts new file mode 100644 index 0000000..76ba3ea --- /dev/null +++ b/src/app/core/models/space-block.ts @@ -0,0 +1,22 @@ +import { Rect } from '../../shared/coordinate'; +import { TransformHelper } from './transform-helper'; +import { TreeNode } from './tree-node'; +export type ISpaceBlockType = 'top' | 'right' | 'bottom' | 'left' | (string & {}); + +export interface ISpaceBlock { + id?: string; + refer?: TreeNode; + rect?: Rect; + distance?: number; + type?: ISpaceBlockType; +} + +export type AroundSpaceBlock = Record; +export class SpaceBlock { + _id: string; + distance: number; + refer: TreeNode; + helper: TransformHelper; + rect: Rect; + type: ISpaceBlockType; +} diff --git a/src/app/core/models/transform-helper.ts b/src/app/core/models/transform-helper.ts new file mode 100644 index 0000000..7a3daf2 --- /dev/null +++ b/src/app/core/models/transform-helper.ts @@ -0,0 +1,62 @@ +import { TreeNode } from './tree-node'; +import { Operation } from './operation'; +import { IPoint, IRect, ISize, Rect } from '../../shared/coordinate'; +import { SnapLine } from './snapline'; +import { AroundSpaceBlock } from './space-block'; + +export interface ITransformHelperProps { + operation: Operation; +} + +export type TransformHelperType = 'translate' | 'resize' | 'rotate' | 'scale' | 'round'; + +export type ResizeDirection = + | 'left-top' + | 'left-center' + | 'left-bottom' + | 'center-top' + | 'center-bottom' + | 'right-top' + | 'right-bottom' + | 'right-center' + | (string & {}); + +export interface ITransformHelperDragStartProps { + type: TransformHelperType; + direction?: ResizeDirection; + dragNodes: TreeNode[]; +} + +export class TransformHelper { + operation: Operation; + + type: TransformHelperType; + + direction: ResizeDirection; + + dragNodes: TreeNode[] = []; + + rulerSnapLines: SnapLine[] = []; + + aroundSnapLines: SnapLine[] = []; + + aroundSpaceBlocks: AroundSpaceBlock = null; + + viewportRectsStore: Record = {}; + + dragStartTranslateStore: Record = {}; + + dragStartSizeStore: Record = {}; + + draggingNodesRect: Rect; + + cacheDragNodesReact: Rect; + + dragStartNodesRect: IRect = null; + + snapping = false; + + dragging = false; + + snapped = false; +} diff --git a/src/app/core/models/viewport.ts b/src/app/core/models/viewport.ts new file mode 100644 index 0000000..8184011 --- /dev/null +++ b/src/app/core/models/viewport.ts @@ -0,0 +1,53 @@ +import { Workspace } from './workspace'; +import { Engine } from './engine'; + +export interface IViewportProps { + engine: Engine; + workspace: Workspace; + viewportElement: HTMLElement; + contentWindow: Window; + nodeIdAttrName: string; + moveSensitive?: boolean; + moveInsertionType?: IViewportMoveInsertionType; +} + +export interface IViewportData { + scrollX?: number; + scrollY?: number; + width?: number; + height?: number; +} + +export type IViewportMoveInsertionType = 'all' | 'inline' | 'block'; + +export class Viewport { + workspace: Workspace; + + engine: Engine; + + contentWindow: Window; + + viewportElement: HTMLElement; + + dragStartSnapshot: IViewportData; + + scrollX = 0; + + scrollY = 0; + + width = 0; + + height = 0; + + mounted = false; + + attachRequest: number; + + nodeIdAttrName: string; + + moveSensitive: boolean; + + moveInsertionType: IViewportMoveInsertionType; + + nodeElementsStore: Record = {}; +} diff --git a/src/app/core/models/workbench.ts b/src/app/core/models/workbench.ts index b86ae68..cc7dd71 100644 --- a/src/app/core/models/workbench.ts +++ b/src/app/core/models/workbench.ts @@ -1,6 +1,7 @@ import { WorkbenchTypes } from '../types'; import { Engine } from './engine'; import { Workspace } from './workspace'; +import { action, define, observable } from '@formily/reactive'; export class Workbench { workspaces: Workspace[]; @@ -12,4 +13,25 @@ export class Workbench { engine: Engine; type: WorkbenchTypes = 'DESIGNABLE'; + + constructor(engine: Engine) { + this.engine = engine; + this.workspaces = []; + this.currentWorkspace = null; + this.activeWorkspace = null; + } + + makeObservable() { + define(this, { + currentWorkspace: observable.ref, + workspaces: observable.shallow, + activeWorkspace: observable.ref, + type: observable.ref + // switchWorkspace: action, + // addWorkspace: action, + // removeWorkspace: action, + // setActiveWorkspace: action, + // setWorkbenchType: action + }); + } } diff --git a/src/app/core/models/workspace.ts b/src/app/core/models/workspace.ts index bbc9961..4377f66 100644 --- a/src/app/core/models/workspace.ts +++ b/src/app/core/models/workspace.ts @@ -1 +1,42 @@ -export class Workspace {} +import { Engine } from '@/app/core/models/engine'; +import { Viewport } from '@/app/core/models/viewport'; +import { Operation } from '@/app/core/models/operation'; +import { History } from '@/app/core/models/history'; + +export interface IWorkspace { + id?: string; + title?: string; + description?: string; + // operation: IOperation +} + +export interface IWorkspaceProps { + id?: string; + title?: string; + description?: string; + contentWindow?: Window; + viewportElement?: HTMLElement; +} + +/** + * 工作区模型 + */ +export class Workspace { + id: string; + + title: string; + + description: string; + + engine: Engine; + + viewport: Viewport; + + outline: Viewport; + + operation: Operation; + + history: History; + + props: IWorkspaceProps; +} diff --git a/src/app/shared/coordinate.ts b/src/app/shared/coordinate.ts new file mode 100644 index 0000000..b3305b8 --- /dev/null +++ b/src/app/shared/coordinate.ts @@ -0,0 +1,50 @@ +export interface IRect { + x: number; + y: number; + width: number; + height: number; +} + +export interface IPoint { + x: number; + y: number; +} + +export interface ISize { + width: number; + height: number; +} + +export interface ILineSegment { + start: IPoint; + end: IPoint; +} + +export class Rect implements IRect { + x = 0; + y = 0; + width = 0; + height = 0; + constructor(x: number, y: number, width: number, height: number) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + get left() { + return this.x; + } + + get right() { + return this.x + this.width; + } + + get top() { + return this.y; + } + + get bottom() { + return this.y + this.height; + } +} diff --git a/src/app/shared/event.ts b/src/app/shared/event.ts index 59070eb..2716a1f 100644 --- a/src/app/shared/event.ts +++ b/src/app/shared/event.ts @@ -1,5 +1,76 @@ +import { ISubscriber } from '@/app/shared/subscription'; + export interface ICustomEvent { type: string; data?: EventData; context?: EventContext; } + +export type EventOptions = + | boolean + | (AddEventListenerOptions & + EventListenerOptions & { + mode?: 'onlyOne' | 'onlyParent' | 'onlyChild'; + }); + +export type EventDriverContainer = HTMLElement | HTMLDocument; + +export interface IEventEffect { + (engine: T): void; +} +export interface IEventDriver { + container: EventDriverContainer; + contentWindow: Window; + attach(container: EventDriverContainer): void; + detach(container: EventDriverContainer): void; + dispatch = any>(event: T): void | boolean; + subscribe = any>(subscriber: ISubscriber): void; + addEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventOptions + ): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventOptions): void; + addEventListener(type: any, listener: any, options: any): void; + removeEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventOptions + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventOptions + ): void; + removeEventListener(type: any, listener: any, options?: any): void; + batchAddEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventOptions + ): void; + batchAddEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventOptions + ): void; + batchAddEventListener(type: any, listener: any, options?: any): void; + batchRemoveEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventOptions + ): void; + batchRemoveEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventOptions + ): void; + batchRemoveEventListener(type: any, listener: any, options: any): void; +} + +export interface IEventDriverClass { + new (engine: T, context?: any): IEventDriver; +} +export interface IEventProps { + drivers?: IEventDriverClass[]; + effects?: IEventEffect[]; +} diff --git a/src/app/shared/subscription.ts b/src/app/shared/subscription.ts new file mode 100644 index 0000000..f2b1b19 --- /dev/null +++ b/src/app/shared/subscription.ts @@ -0,0 +1,3 @@ +export interface ISubscriber { + (payload: Payload): void | boolean; +} diff --git a/src/app/shared/types.ts b/src/app/shared/types.ts index 0ed83e7..8365200 100644 --- a/src/app/shared/types.ts +++ b/src/app/shared/types.ts @@ -1,3 +1,25 @@ +import { ITreeNode } from '@/app/core/models'; +import { ScreenType } from '@/app/core/models/screen'; +import { IEventProps } from '@/app/shared/event'; + +export type IEngineProps = IEventProps & { + shortcuts?: any[]; + sourceIdAttrName?: string; //拖拽源Id的dom属性名 + nodeIdAttrName?: string; //节点Id的dom属性名 + contentEditableAttrName?: string; //原地编辑属性名 + contentEditableNodeIdAttrName?: string; //原地编辑指定Node Id属性名 + clickStopPropagationAttrName?: string; //点击阻止冒泡属性 + outlineNodeIdAttrName?: string; //大纲树节点ID的dom属性名 + nodeSelectionIdAttrName?: string; //节点工具栏属性名 + nodeDragHandlerAttrName?: string; //节点拖拽手柄属性名 + screenResizeHandlerAttrName?: string; + nodeResizeHandlerAttrName?: string; //节点尺寸拖拽手柄属性名 + nodeTranslateAttrName?: string; // 节点自由布局的属性名 + defaultComponentTree?: ITreeNode; //默认组件树 + defaultScreenType?: ScreenType; + rootComponentName?: string; +}; + const isType = (type: string | string[]) => (obj: unknown): obj is T =>