diff --git a/package.json b/package.json index 4dc0107..a75f0f5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "keywords": [ "Vue3", - "fabric.js", + "leafer.js", "Typescript", "Element-plus" ], @@ -34,7 +34,6 @@ "delaunator": "^5.0.0", "dexie": "^3.2.4", "element-plus": "^2.6.1", - "fabric": "^6.0.0-beta12", "file-saver": "^2.0.5", "gifler": "^0.1.0", "hammerjs": "^2.0.8", diff --git a/src/app/attribute/collect.ts b/src/app/attribute/collect.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/attribute/toRef.ts b/src/app/attribute/toRef.ts deleted file mode 100644 index c6f0ed7..0000000 --- a/src/app/attribute/toRef.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { reactive } from 'vue' -import { Object as FabricObject, ObjectRef } from 'fabric' - - -/** - * 元素添加相应式属性 - */ -const toRef = (object: FabricObject) => { - if (object.ref) return object - - const keyArr: (keyof ObjectRef)[] = [ - 'id', - 'name', - 'hideOnLayer', - 'originX', - 'originY', - 'top', - 'left', - 'width', - 'height', - 'scaleX', - 'scaleY', - 'flipX', - 'flipY', - 'opacity', - 'angle', - 'skewX', - 'skewY', - 'hoverCursor', - 'moveCursor', - 'padding', - 'borderColor', - 'borderDashArray', - 'cornerColor', - 'cornerStrokeColor', - 'cornerStyle', - 'cornerDashArray', - 'centeredScaling', - 'centeredRotation', - 'fill', - 'fillRule', - 'globalCompositeOperation', - 'backgroundColor', - 'selectionBackgroundColor', - 'stroke', - 'strokeWidth', - 'strokeDashArray', - 'strokeDashOffset', - 'strokeLineCap', - 'strokeLineJoin', - 'strokeMiterLimit', - 'shadow', - 'borderScaleFactor', - 'minScaleLimit', - 'selectable', - 'evented', - 'visible', - 'hasControls', - 'hasBorders', - 'perPixelTargetFind', - 'includeDefaultValues', - 'lockMovementX', - 'lockMovementY', - 'lockRotation', - 'lockScalingX', - 'lockScalingY', - 'lockSkewingX', - 'lockSkewingY', - 'lockScalingFlip', - 'excludeFromExport', - 'objectCaching', - 'noScaleCache', - 'strokeUniform', - 'dirty', - 'paintFirst', - 'activeOn', - 'colorProperties', - 'inverted', - 'absolutePositioned', - ] - - if (object.isType('Rect')) { - keyArr.push('rx', 'ry') - } - - if (object.isType('Text', 'Textbox')) { - keyArr.push( - 'text', - 'charSpacing', - 'lineHeight', - 'fontSize', - 'fontWeight', - 'fontFamily', - 'fontStyle', - 'pathSide', - 'pathAlign', - 'underline', - 'overline', - 'linethrough', - 'textAlign', - 'direction', - ) - } - - object.ref = reactive({}) as ObjectRef - - keyArr.forEach((key: K) => { - object.ref[key] = object[key] - - Object.defineProperty(object, key, { - get() { - return this.ref[key] - }, - set(value) { - if (this.ref[key] === value) return - this.ref[key] = value - }, - }) - }) - - return object -} - -export { toRef } diff --git a/src/app/fabricCanvas.ts b/src/app/fabricCanvas.ts deleted file mode 100644 index 365a6ef..0000000 --- a/src/app/fabricCanvas.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useMainStore, useTemplatesStore } from '@/store' -import { Canvas, Object as FabricObject, Image } from 'fabric' -import { shallowRef } from 'vue' -import { toRef } from './attribute/toRef' -import { check } from '@/utils/check' -import { nonid } from '@/utils/common' -import { FabricRuler } from './fabricRuler' - -export class FabricCanvas extends Canvas { - public ruler?: FabricRuler - public loading?: Image - public activeObject = shallowRef() - - constructor(el: string | HTMLCanvasElement, options?: any) { - super(el, options) - // const templatesStore = useTemplatesStore() - // this.on('object:modified', () => templatesStore.modifedElement()) - } - - // @ts-ignore - public get _activeObject() { - return this.activeObject ? this.activeObject.value : undefined - } - - public set _activeObject(value) { - const mainStore = useMainStore() - mainStore.setCanvasObject(value as FabricObject) - this.activeObject.value = value - } - - override add(...objects: FabricObject[]): number { - return super.add( - ...objects.map((obj) => { - this.setDefaultAttr(obj) - return toRef(obj) - }), - ) - } - - override insertAt(index: number, ...objects: FabricObject[]): number { - return super.insertAt( - index, - ...objects.map((obj) => { - this.setDefaultAttr(obj) - return toRef(obj) - }), - ) - } - - private setDefaultAttr(target: FabricObject) { - // 添加name - if (!target.name) { - target.set({name: target.type}) - } - // 添加id - if (!target.id) { - target.set({id: nonid(8)}) - } - if (check.isTextObject(target)) { - target.set({color: target.fill}) - } - if (check.isCollection(target)) { - target._objects.forEach((obj) => { - this.setDefaultAttr(obj) - }) - } - } -} \ No newline at end of file diff --git a/src/app/fabricCommand.ts b/src/app/fabricCommand.ts deleted file mode 100644 index 6fb1995..0000000 --- a/src/app/fabricCommand.ts +++ /dev/null @@ -1,55 +0,0 @@ - -import { Object as FabricObject } from "fabric"; - -class FabricCommand { - public receiver: FabricObject - public state: any - public prevState: any - public stateProperties: any - constructor(receiver: FabricObject) { - this.receiver = receiver; - - this._initStateProperties(); - this.state = {}; - this.prevState = {}; - - this._saveState(); - this._savePrevState(); - } - execute() { - this._restoreState(); - this.receiver.setCoords(); - } - undo() { - this._restorePrevState(); - this.receiver.setCoords(); - } - _initStateProperties() { - this.stateProperties = Object.keys(this.receiver._stateProperties); - } - _restoreState() { - this._restore(this.state); - } - _restorePrevState() { - this._restore(this.prevState); - } - _restore(state: any) { - this.stateProperties.forEach(prop => { - this.receiver.set(prop, state[prop]); - }); - } - _saveState() { - this.stateProperties.forEach(prop => { - this.state[prop] = this.receiver.get(prop); - }); - } - _savePrevState() { - if (this.receiver._stateProperties) { - this.stateProperties.forEach(prop => { - this.prevState[prop] = this.receiver._stateProperties[prop]; - }); - } - } -} - -export default FabricCommand; diff --git a/src/app/fabricControls.ts b/src/app/fabricControls.ts deleted file mode 100644 index ad2d705..0000000 --- a/src/app/fabricControls.ts +++ /dev/null @@ -1,491 +0,0 @@ -import { noop } from '@vueuse/core' -import { TControlSet } from '@/types/fabric' -import { PolygonElement } from '@/types/canvas' -import { PiBy180, toFixed } from '@/utils/common' -import { px2mm } from '@/utils/image' -import { Control, Object as FabricObject, controlsUtils, Point, Polygon, TPointerEvent, Transform, TDegree, util,TransformActionHandler } from 'fabric' -import { storeToRefs } from 'pinia' -import { useMainStore } from '@/store' -import { ArcText } from '@/extension/object/ArcText' - - -export const changeObjectHeight: TransformActionHandler = (eventData: TPointerEvent, transform: Transform, x: number, y: number) => { - const localPoint = controlsUtils.getLocalPoint(transform, transform.originX, transform.originY, x, y); - - // make sure the control changes width ONLY from it's side of target - const { target } = transform - if ((transform.originY === 'top' && localPoint.y > 0) || (transform.originY === 'bottom' && localPoint.y < 0)) { - - const strokeWidth = target.strokeWidth ? target.strokeWidth : 0 - if (!target.scaleY) return false - const strokePadding = strokeWidth / (target.strokeUniform ? target.scaleY : 1) - const oldHeight = target.height - const newHeight = Math.ceil(Math.abs((localPoint.y * 1) / target.scaleY) - strokePadding) - target.set('height', Math.max(newHeight, 0)) - return oldHeight !== target.height; - } - return false; -}; - -export const changeObjectCurvature: TransformActionHandler = (eventData: TPointerEvent, transform: Transform, x: number, y: number) => { - const target = transform.target as ArcText - let localPoint = controlsUtils.getLocalPoint(transform, transform.originX, transform.originY, x, y), - strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), - multiplier = transform.originY === 'center' ? 2 : 1, - cy = (localPoint.y + target.controls[transform.corner].offsetY - target.height / 2 + target._contentOffsetY ) * multiplier / target.scaleY - strokePadding; - - let textHeight = target.calcTextHeight(); - - let radius; - if (Math.abs(cy) <= textHeight / 2) { - radius = 0; - } - else{ - radius = cy > 0 ? cy - textHeight / 2 : cy + textHeight / 2; - } - - target.set(radius) - return false -} - -// define a function that can locate the controls. -// this function will be used both for drawing and for interaction. -export function polygonPositionHandler(dim: Point, finalMatrix: number[], fabricObject: any) { - // @ts-ignore - const pointIndex = this.pointIndex - - const x = (fabricObject.points[pointIndex].x - fabricObject.pathOffset.x) - const y = (fabricObject.points[pointIndex].y - fabricObject.pathOffset.y) - - const point = util.transformPoint( - { x, y } as Point, - util.multiplyTransformMatrices( - fabricObject.canvas.viewportTransform, - fabricObject.calcTransformMatrix() - ) - ) - const snapPoint = fabricObject.pointMoving(pointIndex, point) - // console.log('Point:', point, 'x:', x, 'y:', y, snapPoint) - return point -} - -const getObjectSizeWithStroke = (object: FabricObject) => { - const scaleX = object.scaleX, scaleY = object.scaleY, strokeWidth = object.strokeWidth - const width = object.width, height = object.height - const stroke = new Point( - object.strokeUniform ? 1 / scaleX : 1, - object.strokeUniform ? 1 / scaleY : 1 - ).scalarMultiply(strokeWidth); - return new Point(width + stroke.x, height + stroke.y); -} - -// define a function that can keep the polygon in the same position when we change its -// width/height/top/left. -export const anchorWrapper = (anchorIndex: number, fn: Function) => { - - return function(eventData: MouseEvent, transform: any, x: number, y: number) { - - const fabricObject = transform.target as Polygon - const pointX = fabricObject.points[anchorIndex].x, pointY = fabricObject.points[anchorIndex].y - const handlePoint = new Point({x: (pointX - fabricObject.pathOffset.x), y: (pointY - fabricObject.pathOffset.y)}) - const absolutePoint = util.transformPoint(handlePoint, fabricObject.calcTransformMatrix()), - actionPerformed = fn(eventData, transform, x, y), - newDim = fabricObject.setDimensions(), - polygonBaseSize = getObjectSizeWithStroke(fabricObject), - newX = (pointX - fabricObject.pathOffset.x) / polygonBaseSize.x, - newY = (pointY - fabricObject.pathOffset.y) / polygonBaseSize.y - fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5) - return actionPerformed - } -} - -export const actionHandler = (eventData: TPointerEvent, transform: any, x: number, y: number) => { - const polygon = transform.target as PolygonElement - if (!polygon.__corner) return - const currentControl = polygon.controls[polygon.__corner] - const mouseLocalPosition = controlsUtils.getLocalPoint(transform, 'center', 'center', x, y) - // const mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center') - const polygonBaseSize = getObjectSizeWithStroke(polygon) - - const size = polygon._getTransformedDimensions(0) - const finalPointPosition = { - x: mouseLocalPosition.x * polygonBaseSize.x / size.x + polygon.pathOffset.x, - y: mouseLocalPosition.y * polygonBaseSize.y / size.y + polygon.pathOffset.y - } as Point - polygon.points[currentControl.pointIndex as number] = finalPointPosition - return true -} - -/** - * 计算当前控件的位置 - */ -const positionHandler: Control['positionHandler'] = (dim, finalMatrix, fabricObject, currentControl) => { - return new Point( - currentControl.x * dim.x + currentControl.offsetX, - currentControl.y * dim.y + currentControl.offsetY, - ).transform(finalMatrix) -} - -export const getWidthHeight = (fabricObject: FabricObject, noFixed = false) => { - const objScale = fabricObject.getObjectScaling() - const point = fabricObject._getTransformedDimensions({ - scaleX: objScale.x, - scaleY: objScale.y, - }) - if (!noFixed) { - point.setX(toFixed(point.x)) - point.setY(toFixed(point.y)) - } - return point -} - -/** - * 更新ml, mr, mt, mb的控件大小 - */ -const setCornersSize = (object: FabricObject) => { - if (!object.canvas) return - const zoom = object.canvas.getZoom() - const size = getWidthHeight(object).scalarMultiply(zoom) - const controls = object.controls - const cornersH = ['ml', 'mr'] - cornersH.forEach((corner) => { - controls[corner].sizeX = object.cornerSize - controls[corner].sizeY = size.y - controls[corner].touchSizeX = object.touchCornerSize - controls[corner].touchSizeY = size.y - }) - const cornersV = ['mt', 'mb'] - cornersV.forEach((corner) => { - controls[corner].sizeX = size.x - controls[corner].sizeY = object.cornerSize - controls[corner].touchSizeX = size.x - controls[corner].touchSizeY = object.touchCornerSize - }) -} - -/** - * 旋转图标 - */ -const rotateIcon = (angle: number) => { - return `url("data:image/svg+xml,") 12 12,auto` -} - -/** - * 旋转吸附,按住shift键,吸附15度角 - */ -const rotationWithSnapping = (eventData: TPointerEvent, transform: Transform, x: number, y: number) => { - const { shiftKey } = eventData - const { target } = transform - const { rotationWithSnapping } = controlsUtils - let snapAngle: TDegree | undefined - if (shiftKey) { - snapAngle = target.snapAngle - target.snapAngle = 15 - } - const res = rotationWithSnapping(eventData, transform, x, y) - snapAngle && (target.snapAngle = snapAngle) - return res -} - -/** - * 获取旋转控件 - */ -const getRotateControl = (angle: number): Partial => ({ - sizeX: 16, - sizeY: 16, - actionHandler: (eventData, transformData, x, y) => { - transformData.target.canvas?.setCursor(rotateIcon(transformData.target.angle + angle)) - return rotationWithSnapping(eventData, transformData, x, y) - }, - cursorStyleHandler: (eventData, control, fabricObject) => { - return rotateIcon(fabricObject.angle + angle) - }, - render: noop, - actionName: 'rotate', -}) - -/** - * 获取通用控件属性 - */ -const getHornControl = { - cursorStyleHandler: controlsUtils.scaleCursorStyleHandler, - actionHandler: controlsUtils.scalingEqually, - actionName: 'scaling', -} - -const changeWidth = controlsUtils.wrapWithFireEvent( - 'scaling', - controlsUtils.wrapWithFixedAnchor(controlsUtils.changeWidth), -) - -const changeHeight = controlsUtils.wrapWithFireEvent( - 'scaling', - controlsUtils.wrapWithFixedAnchor(changeObjectHeight) -) - -const changeCurvature = controlsUtils.wrapWithFireEvent( - 'scaling', - controlsUtils.wrapWithFixedAnchor(changeObjectCurvature) -) - - -export const defaultControls = (): TControlSet => ({ - size: new Control({ - x: 0, - y: 0.5, - cursorStyleHandler: () => '', - offsetY: 14, - sizeX: 0.0001, - sizeY: 0.0001, - touchSizeX: 0.0001, - touchSizeY: 0.0001, - render: (ctx, left, top, styleOverride, fabricObject: FabricObject) => { - // todo: 支持组内反转的对象 - ctx.save() - ctx.translate(left, top) - - const calcRotate = () => { - const objectAngle = fabricObject.group ? fabricObject.getTotalAngle() : fabricObject.angle - const angleInRadians = objectAngle * PiBy180 - const x = Math.sin(angleInRadians) - const y = Math.cos(angleInRadians) - const angle = Math.abs(x) > Math.abs(y) ? Math.sign(x) * 90 : Math.sign(y) * 90 - 90 - return (objectAngle - angle) * PiBy180 - } - - ctx.rotate(calcRotate()) - - const fontSize = 12 - ctx.font = `${fontSize}px Tahoma` - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - - const { x, y } = getWidthHeight(fabricObject) - const { unitMode } = storeToRefs(useMainStore()) - let text = unitMode.value === 0 ? `${toFixed(px2mm(x))} × ${toFixed(px2mm(y))}` : `${x} × ${y}` - const width = ctx.measureText(text).width + 8 - const height = fontSize + 6 - - // 背景 - //@ts-ignore - ctx.roundRect(-width / 2, -height / 2, width, height, 4) - ctx.fillStyle = '#0066ff' - ctx.fill() - - // 文字 - ctx.fillStyle = '#fff' - ctx.fillText(text, 0, 1) - ctx.restore() - }, - positionHandler: (dim, finalMatrix, fabricObject: FabricObject, currentControl) => { - const activeObject = fabricObject.canvas?.getActiveObject instanceof Function ? fabricObject.canvas?.getActiveObject() : null - - if (activeObject && activeObject === fabricObject) { - const angle = fabricObject.getTotalAngle() - - const angleInRadians = angle * PiBy180 - - const x = Math.sin(angleInRadians) - const y = Math.cos(angleInRadians) - - if (Math.abs(x) >= Math.abs(y)) { - const sign = Math.sign(x) - currentControl.x = sign / 2 - currentControl.y = 0 - currentControl.offsetX = sign * 14 - currentControl.offsetY = 0 - } else { - const sign = Math.sign(y) - currentControl.x = 0 - currentControl.y = sign / 2 - currentControl.offsetX = 0 - currentControl.offsetY = sign * 14 - } - - // 更新其它corners大小,放到这里一起更新,来防止多次运行 - setCornersSize(fabricObject) - } - - return positionHandler(dim, finalMatrix, fabricObject, currentControl) - }, - }), - - tlr: new Control({ - x: -0.5, - y: -0.5, - offsetX: -4, - offsetY: -4, - ...getRotateControl(0), - }), - - trr: new Control({ - x: 0.5, - y: -0.5, - offsetX: 4, - offsetY: -4, - ...getRotateControl(90), - }), - - brr: new Control({ - x: 0.5, - y: 0.5, - offsetX: 4, - offsetY: 4, - ...getRotateControl(180), - }), - - blr: new Control({ - x: -0.5, - y: 0.5, - offsetX: -4, - offsetY: 4, - ...getRotateControl(270), - }), - - ml: new Control({ - x: -0.5, - y: 0, - actionHandler: controlsUtils.scalingXOrSkewingY, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - actionName: 'scaling', - render: noop, - // 不在这里设置positionHandler,放到size的positionHandler一起更新 - // positionHandler: positionHandlerH, - }), - - mr: new Control({ - x: 0.5, - y: 0, - actionHandler: controlsUtils.scalingXOrSkewingY, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - actionName: 'scaling', - render: noop, - // positionHandler: positionHandlerH, - }), - - mb: new Control({ - x: 0, - y: 0.5, - actionHandler: controlsUtils.scalingYOrSkewingX, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - actionName: 'scaling', - render: noop, - // positionHandler: positionHandlerV, - }), - - mt: new Control({ - x: 0, - y: -0.5, - actionHandler: controlsUtils.scalingYOrSkewingX, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - actionName: 'scaling', - render: noop, - // positionHandler: positionHandlerV, - }), - - tl: new Control({ - x: -0.5, - y: -0.5, - ...getHornControl, - }), - - tr: new Control({ - x: 0.5, - y: -0.5, - ...getHornControl, - }), - - bl: new Control({ - x: -0.5, - y: 0.5, - ...getHornControl, - }), - - br: new Control({ - x: 0.5, - y: 0.5, - ...getHornControl, - }), -}) - -export const resizeControls = (): TControlSet => ({ - mr: new Control({ - x: 0.5, - y: 0, - actionHandler: changeWidth, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - render: noop, - // positionHandler: positionHandlerH, - }), - ml: new Control({ - x: -0.5, - y: 0, - actionHandler: changeWidth, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - render: noop, - // positionHandler: positionHandlerH, - }), - mt: new Control({ - x: 0, - y: -0.5, - actionHandler: changeHeight, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - render: noop, - // positionHandler: positionHandlerH, - }), - mb: new Control({ - x: 0, - y: 0.5, - actionHandler: changeHeight, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - render: noop, - // positionHandler: positionHandlerH, - }), -}) - -export const arcTextControls = (): TControlSet => ({ - c: new Control({ - x: 0, - y: 0, - offsetX: 0, - offsetY: 0, - // render (ctx: CanvasRenderingContext2D, left: number, top: number, styleOverride: any, fabricObject: ArcText) { - // if(fabricObject.canvas!.showControlsGuidlines){ - // ctx.save() - // ctx.strokeStyle = fabricObject.borderColor - // ctx.lineWidth = fabricObject.borderWidth - // // let cx = -fabricObject._contentOffsetX * fabricObject.scaleX - // // let cy = (fabricObject._curvingCenter.y - fabricObject._contentOffsetY) * fabricObject.scaleY - // ctx.beginPath() - // ctx.ellipse(left, top, Math.abs(fabricObject.radius) * fabricObject.scaleX, Math.abs(fabricObject.radius) * fabricObject.scaleY, 0, 0, 2 * Math.PI); - // ctx.stroke(); - // ctx.restore() - // } - // }, - actionHandler: changeCurvature, - cursorStyle: 'pointer', - actionName: 'resizing', - }), - ...defaultControls(), - ...resizeControls(), -}) - -export const lineControls = (): TControlSet => ({ - ml: new Control({ - x: -0.5, - y: 0, - actionHandler: changeWidth, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - }), - mr: new Control({ - x: 0.5, - y: 0, - actionHandler: changeWidth, - cursorStyleHandler: controlsUtils.scaleSkewCursorStyleHandler, - }), -}) - -export const textboxControls = (): TControlSet => ({ - ...defaultControls(), - ...resizeControls(), -}) diff --git a/src/app/fabricGuide.ts b/src/app/fabricGuide.ts deleted file mode 100644 index bf9fc5d..0000000 --- a/src/app/fabricGuide.ts +++ /dev/null @@ -1,411 +0,0 @@ - -import { StaticCanvas, Canvas, ActiveSelection, Object as FabricObject, Group, Point, util } from 'fabric' -import { Disposable } from '@/utils/lifecycle' -import { check } from '@/utils/check' - -type VerticalLineCoords = { - x: number - y1: number - y2: number -} - -type HorizontalLineCoords = { - y: number - x1: number - x2: number -} - -type IgnoreObjTypes = { - key: T - value: any -}[] - -type ACoordsAppendCenter = NonNullable & { - c: Point -} - -const Keys = (obj: T): (keyof T)[] => { - return Object.keys(obj) as (keyof T)[] -} - -export class FabricGuide extends Disposable { - private canvasEvents - - private aligningLineMargin = 10 - private aligningLineWidth = 1 - private aligningLineColor = '#F68066' - - private verticalLines: VerticalLineCoords[] = [] - private horizontalLines: HorizontalLineCoords[] = [] - private activeObj: FabricObject | undefined - private ignoreObjTypes: IgnoreObjTypes = [] - private pickObjTypes: IgnoreObjTypes = [] - private dirty = false - - constructor(private readonly canvas: Canvas) { - super() - - const mouseUp = () => { - if (this.horizontalLines.length || this.verticalLines.length) { - this.clearGuideline() - this.clearStretLine() - } - } - - this.canvasEvents = { - 'before:render': this.clearGuideline.bind(this), - 'after:render': this.drawGuideLines.bind(this), - 'object:moving': this.objectMoving.bind(this), - 'mouse:up': mouseUp, - } - - canvas.on(this.canvasEvents as any) - } - - private objectMoving({ target }: any) { - this.clearStretLine() - - const transform = this.canvas._currentTransform - if (!transform) return - - this.activeObj = target - - const activeObjects = this.canvas.getActiveObjects() - - const canvasObjects: FabricObject[] = [] - const add = (group: Group | Canvas | StaticCanvas | ActiveSelection) => { - const objects = group.getObjects().filter((obj) => { - if (this.ignoreObjTypes.length) { - return !this.ignoreObjTypes.some((item) => obj.get(item.key) === item.value) - } - if (this.pickObjTypes.length) { - return this.pickObjTypes.some((item) => obj.get(item.key) === item.value) - } - // 排除 自己 和 激活选区内的元素 - if (activeObjects.includes(obj)) { - return false - } - if (!obj.visible) { - return false - } - // 元素为组,把组内元素加入,同时排除组本身 - if (check.isActiveSelection(obj)) { - add(obj) - return false - } - // 元素为组,把组内元素加入,同时排除组本身 - if (check.isCollection(obj) && target.group && obj === target.group) { - add(obj) - return false - } - return true - }) - canvasObjects.push(...objects as FabricObject[]) - } - - if (check.isActiveSelection(target)) { - const needAddParent = new Set() - target.forEachObject((obj) => { - const parent = obj.getParent() - if (parent) needAddParent.add(parent as Group) - }) - needAddParent.forEach((parent) => { - if (check.isNativeGroup(parent)) { - canvasObjects.push(parent) - } - add(parent) - }) - } else { - const parent = target.getParent() as Group - if (check.isNativeGroup(parent)) { - canvasObjects.push(parent) - } - add(parent) - } - - this.traversAllObjects(target, canvasObjects) - } - - private clearStretLine() { - this.verticalLines.length = this.horizontalLines.length = 0 - } - - private getObjDraggingObjCoords(activeObject: FabricObject): ACoordsAppendCenter { - const coords = this.getCoords(activeObject) - const centerPoint = this.calcCenterPointByACoords(coords).subtract(activeObject.getCenterPoint()) - const newCoords = Keys(coords).map((key) => coords[key].subtract(centerPoint)) - return { - tl: newCoords[0], - tr: newCoords[1], - br: newCoords[2], - bl: newCoords[3], - c: activeObject.getCenterPoint(), - } - } - - private getObjMaxWidthHeightByCoords(coords: ACoordsAppendCenter) { - const { c, tl, tr } = coords - const objHeight = Math.max(Math.abs(c.y - tl.y), Math.abs(c.y - tr.y)) * 2 - const objWidth = Math.max(Math.abs(c.x - tl.x), Math.abs(c.x - tr.x)) * 2 - return { objHeight, objWidth } - } - - // 当对象被旋转时,需要忽略一些坐标,例如水平辅助线只取最上、下边的坐标(参考 figma) - private omitCoords(objCoords: ACoordsAppendCenter, type: 'vertical' | 'horizontal') { - const newCoords = objCoords - const axis = type === 'vertical' ? 'x' : 'y' - Keys(objCoords).forEach((key) => { - if (objCoords[key][axis] < newCoords.tl[axis]) { - newCoords[key] = objCoords[key] - } - if (objCoords[key][axis] > newCoords.tl[axis]) { - newCoords[key] = objCoords[key] - } - }) - return newCoords - } - - /** - * 检查 value1 和 value2 是否在指定的范围内,用于对齐线的计算 - */ - private isInRange(value1: number, value2: number) { - return ( - Math.abs(Math.round(value1) - Math.round(value2)) <= - this.aligningLineMargin / this.canvas.getZoom() - ) - } - - private getCoords(obj: FabricObject) { - const [tl, tr, br, bl] = obj.getCoords(true) - return { tl, tr, br, bl } - } - - /** - * fabric.Object.getCenterPoint will return the center point of the object calc by mouse moving & dragging distance. - * calcCenterPointByACoords will return real center point of the object position. - */ - private calcCenterPointByACoords(coords: NonNullable): Point { - return new Point((coords.tl.x + coords.br.x) / 2, (coords.tl.y + coords.br.y) / 2) - } - - private traversAllObjects(activeObject: FabricObject, canvasObjects: FabricObject[]) { - const objCoordsByMovingDistance = this.getObjDraggingObjCoords(activeObject) - const snapXPoints: Set = new Set() - const snapYPoints: Set = new Set() - - for (let i = canvasObjects.length; i--;) { - const objCoords = { - ...this.getCoords(canvasObjects[i]), - c: canvasObjects[i].getCenterPoint(), - } as ACoordsAppendCenter - const { objHeight, objWidth } = this.getObjMaxWidthHeightByCoords(objCoords) - Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => { - const newCoords = - canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, 'horizontal') : objCoords - - function calcHorizontalLineCoords( - objPoint: keyof ACoordsAppendCenter, - activeObjCoords: ACoordsAppendCenter, - ) { - let x1: number, x2: number - if (objPoint === 'c') { - x1 = Math.min(objCoords.c.x - objWidth / 2, activeObjCoords[activeObjPoint].x) - x2 = Math.max(objCoords.c.x + objWidth / 2, activeObjCoords[activeObjPoint].x) - } else { - x1 = Math.min(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x) - x2 = Math.max(objCoords[objPoint].x, activeObjCoords[activeObjPoint].x) - } - return { x1, x2 } - } - - Keys(newCoords).forEach((objPoint) => { - if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].y, objCoords[objPoint].y)) { - const y = objCoords[objPoint].y - - const offset = objCoordsByMovingDistance[activeObjPoint].y - y - snapYPoints.add(objCoordsByMovingDistance.c.y - offset) - - const aCoords = this.getCoords(activeObject) - const { x1, x2 } = calcHorizontalLineCoords(objPoint, { - ...aCoords, - c: this.calcCenterPointByACoords(aCoords), - } as ACoordsAppendCenter) - this.horizontalLines.push({ y, x1, x2 }) - } - }) - }) - - Keys(objCoordsByMovingDistance).forEach((activeObjPoint) => { - const newCoords = - canvasObjects[i].angle !== 0 ? this.omitCoords(objCoords, 'vertical') : objCoords - - function calcVerticalLineCoords( - objPoint: keyof ACoordsAppendCenter, - activeObjCoords: ACoordsAppendCenter, - ) { - let y1: number, y2: number - if (objPoint === 'c') { - y1 = Math.min(newCoords.c.y - objHeight / 2, activeObjCoords[activeObjPoint].y) - y2 = Math.max(newCoords.c.y + objHeight / 2, activeObjCoords[activeObjPoint].y) - } else { - y1 = Math.min(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y) - y2 = Math.max(objCoords[objPoint].y, activeObjCoords[activeObjPoint].y) - } - return { y1, y2 } - } - - Keys(newCoords).forEach((objPoint) => { - if (this.isInRange(objCoordsByMovingDistance[activeObjPoint].x, objCoords[objPoint].x)) { - const x = objCoords[objPoint].x - - const offset = objCoordsByMovingDistance[activeObjPoint].x - x - snapXPoints.add(objCoordsByMovingDistance.c.x - offset) - - const aCoords = this.getCoords(activeObject) - const { y1, y2 } = calcVerticalLineCoords(objPoint, { - ...aCoords, - c: this.calcCenterPointByACoords(aCoords), - } as ACoordsAppendCenter) - this.verticalLines.push({ x, y1, y2 }) - } - }) - }) - } - - this.snap({ - activeObject, - draggingObjCoords: objCoordsByMovingDistance, - snapXPoints, - snapYPoints, - }) - } - - /** - * 自动吸附对象 - */ - private snap({ - activeObject, - draggingObjCoords, - snapXPoints, - snapYPoints, - }: { - /** 当前活动对象 */ - activeObject: FabricObject - /** 活动对象的坐标 */ - draggingObjCoords: ACoordsAppendCenter - /** 横向吸附点列表 */ - snapXPoints: Set - /** 纵向吸附点列表 */ - snapYPoints: Set - }) { - if (snapXPoints.size === 0 && snapYPoints.size === 0) return - - // 获得最近的吸附点 - const sortPoints = (list: Set, originPoint: number): number => { - if (list.size === 0) { - return originPoint - } - - const sortedList = [...list].sort( - (a, b) => Math.abs(originPoint - a) - Math.abs(originPoint - b), - ) - - return sortedList[0] - } - - // auto snap nearest object, record all the snap points, and then find the nearest one - activeObject.setXY( - new Point( - sortPoints(snapXPoints, draggingObjCoords.c.x), - sortPoints(snapYPoints, draggingObjCoords.c.y), - ), - 'center', - 'center', - ) - } - - private drawSign(x: number, y: number) { - const ctx = this.canvas.getTopContext() - - ctx.strokeStyle = this.aligningLineColor - ctx.beginPath() - - const size = 3 - ctx.moveTo(x - size, y - size) - ctx.lineTo(x + size, y + size) - ctx.moveTo(x + size, y - size) - ctx.lineTo(x - size, y + size) - ctx.stroke() - } - - private drawLine(x1: number, y1: number, x2: number, y2: number) { - const ctx = this.canvas.getTopContext() - const point1 = util.transformPoint(new Point(x1, y1), this.canvas.viewportTransform) - const point2 = util.transformPoint(new Point(x2, y2), this.canvas.viewportTransform) - - // use origin canvas api to draw guideline - ctx.save() - ctx.lineWidth = this.aligningLineWidth - ctx.strokeStyle = this.aligningLineColor - ctx.beginPath() - - ctx.moveTo(point1.x, point1.y) - ctx.lineTo(point2.x, point2.y) - - ctx.stroke() - - this.drawSign(point1.x, point1.y) - this.drawSign(point2.x, point2.y) - - ctx.restore() - - this.dirty = true - } - - private drawVerticalLine(coords: VerticalLineCoords, movingCoords: ACoordsAppendCenter) { - if (!Object.values(movingCoords).some((coord) => Math.abs(coord.x - coords.x) < 0.0001)) return - this.drawLine( - coords.x, - Math.min(coords.y1, coords.y2), - coords.x, - Math.max(coords.y1, coords.y2), - ) - } - - private drawHorizontalLine(coords: HorizontalLineCoords, movingCoords: ACoordsAppendCenter) { - if (!Object.values(movingCoords).some((coord) => Math.abs(coord.y - coords.y) < 0.0001)) return - this.drawLine( - Math.min(coords.x1, coords.x2), - coords.y, - Math.max(coords.x1, coords.x2), - coords.y, - ) - } - - private drawGuideLines(e: any) { - if (!e.ctx || (!this.verticalLines.length && !this.horizontalLines.length) || !this.activeObj) { - return - } - - const movingCoords = this.getObjDraggingObjCoords(this.activeObj) - - for (let i = this.verticalLines.length; i--;) { - this.drawVerticalLine(this.verticalLines[i], movingCoords) - } - for (let i = this.horizontalLines.length; i--;) { - this.drawHorizontalLine(this.horizontalLines[i], movingCoords) - } - // this.canvas.calcOffset() - } - - private clearGuideline() { - if (!this.dirty) return - this.dirty = false - this.canvas.clearContext(this.canvas.getTopContext()) - } - - public dispose(): void { - super.dispose() - this.canvas.off(this.canvasEvents) - } -} diff --git a/src/app/fabricHistory.ts b/src/app/fabricHistory.ts deleted file mode 100644 index 1fd5628..0000000 --- a/src/app/fabricHistory.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import FabricCommand from './fabricCommand' - -class FabricHistory { - public index - public commands: FabricCommand[] - constructor() { - this.commands = []; - this.index = 0; - } - getIndex() { - return this.index; - } - back() { - if (this.index > 0) { - const command = this.commands[--this.index]; - command.undo(); - } - return this; - } - forward() { - if (this.index < this.commands.length) { - const command = this.commands[this.index++]; - command.execute(); - } - return this; - } - add(command: FabricCommand) { - if (this.commands.length) { - this.commands.splice(this.index, this.commands.length - this.index); - } - this.commands.push(command); - this.index++; - return this; - } - clear() { - this.commands.length = 0; - this.index = 0; - return this; - } -} - -export default FabricHistory; diff --git a/src/app/fabricRuler.ts b/src/app/fabricRuler.ts deleted file mode 100644 index a611f12..0000000 --- a/src/app/fabricRuler.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { Keybinding } from './keybinding' -import { Disposable } from '@/utils/lifecycle' -import { computed, watchEffect } from 'vue' -// import { useThemes } from '@/hooks/useThemes' -import { DesignUnitMode } from '@/configs/background' -import { PiBy180, isMobile } from '@/utils/common' -import { TAxis, Point, Rect as fabricRect, Object as FabricObject, TPointerEventInfo, TPointerEvent } from 'fabric' -import { useMainStore, useTemplatesStore } from '@/store' -import { storeToRefs } from 'pinia' -import { px2mm } from '@/utils/image' -import { ElementNames } from '@/types/elements' -import { FabricCanvas } from './fabricCanvas' -import { ReferenceLine } from '@/extension/object/ReferenceLine' -import { WorkSpaceDrawType } from '@/configs/canvas' - -type Rect = { left: number; top: number; width: number; height: number } - -/** - * 配置 - */ -export interface RulerOptions { - /** - * 标尺宽高 - * @default 10 - */ - ruleSize?: number - - /** - * 字体大小 - * @default 10 - */ - fontSize?: number - - /** - * 是否开启标尺 - * @default false - */ - enabled?: boolean - - /** - * 背景颜色 - */ - backgroundColor?: string - - /** - * 文字颜色 - */ - textColor?: string - - /** - * 边框颜色 - */ - borderColor?: string - - /** - * 高亮颜色 - */ - highlightColor?: string - /** - * 高亮颜色 - */ - unitName: string - -} - -export type HighlightRect = {skip?: TAxis} & Rect - -export class FabricRuler extends Disposable { - private canvasEvents - public lastCursor: string - public workSpaceDraw?: fabricRect - public options: Required - public tempReferenceLine?: ReferenceLine - private activeOn: string = "up" - private objectRect: undefined | { - x: HighlightRect[], - y: HighlightRect[] - } - - constructor(private readonly canvas: FabricCanvas) { - super() - this.lastCursor = this.canvas.defaultCursor - // 合并默认配置 - this.options = Object.assign({ - ruleSize: 20, - fontSize: 8, - enabled: isMobile() ? false : true, - }) - - // const { isDark } = useThemes() - const isDark = false - - const { unitMode } = storeToRefs(useMainStore()) - watchEffect(() => { - const unitName = DesignUnitMode.filter(ele => ele.id === unitMode.value)[0].name - this.options = { - ...this.options, - ...(isDark - ? { - backgroundColor: '#242424', - borderColor: '#555', - highlightColor: '#165dff3b', - textColor: '#ddd', - unitName: unitName, - } - : { - backgroundColor: '#fff', - borderColor: '#ccc', - highlightColor: '#165dff3b', - textColor: '#444', - unitName: unitName, - }), - } - this.render({ ctx: this.canvas.contextContainer }) - }) - // computed(() => { - // this.options.unit = unitName - // this.render({ ctx: this.canvas.contextContainer }) - // }) - - this.canvasEvents = { - 'after:render': this.render.bind(this), - 'mouse:move': this.mouseMove.bind(this), - 'mouse:down': this.mouseDown.bind(this), - 'mouse:up': this.mouseUp.bind(this), - 'referenceline:moving': this.referenceLineMoving.bind(this), - 'referenceline:mouseup': this.referenceLineMouseup.bind(this), - } - this.enabled = this.options.enabled - canvas.ruler = this - } - - public getPointHover(point: Point): 'vertical' | 'horizontal' | '' { - if ( - new fabricRect({ - left: 0, - top: 0, - width: this.options.ruleSize, - height: this.canvas.height, - absolutePositioned: true, - }).containsPoint(point) - ) { - return 'vertical'; - } else if ( - new fabricRect({ - left: 0, - top: 0, - width: this.canvas.width, - height: this.options.ruleSize, - absolutePositioned: true, - }).containsPoint(point) - ) { - return 'horizontal'; - } - return ''; - } - - private mouseMove(e: TPointerEventInfo) { - if (!e.pointer) return - if (this.tempReferenceLine && e.absolutePointer) { - const pos: Partial = {}; - if (this.tempReferenceLine.axis === 'horizontal') { - pos.top = e.pointer.y; - } - else { - pos.left = e.pointer.x; - } - this.tempReferenceLine.set({ ...pos, visible: true }); - this.canvas.renderAll(); - const event = this.getCommonEventInfo(e) as any; - this.canvas.fire('object:moving', event); - this.tempReferenceLine.fire('moving', event); - } - const status = this.getPointHover(e.absolutePointer) - this.canvas.defaultCursor = this.lastCursor - if (!status) return - this.lastCursor = this.canvas.defaultCursor - this.canvas.defaultCursor = status === 'horizontal' ? 'ns-resize' : 'ew-resize'; - } - - private mouseDown(e: TPointerEventInfo) { - const pointHover = this.getPointHover(e.absolutePointer) - if (!pointHover) return - if (this.activeOn === 'up') { - this.canvas.selection = false - this.activeOn = 'down' - const point = pointHover === 'horizontal' ? e.pointer.y : e.pointer.x - this.tempReferenceLine = new ReferenceLine( - point, - { - type: 'ReferenceLine', - axis: pointHover, - visible: false, - name: 'ReferenceLine', - hasControls: false, - hasBorders: false, - stroke: 'pink', - fill: 'pink', - originX: 'center', - originY: 'center', - padding: 4, - globalCompositeOperation: 'difference', - } - ); - this.canvas.add(this.tempReferenceLine) - const templatesStore = useTemplatesStore() - templatesStore.modifedElement() - this.canvas.setActiveObject(this.tempReferenceLine) - this.canvas._setupCurrentTransform(e.e, this.tempReferenceLine, true) - this.tempReferenceLine.fire('down', this.getCommonEventInfo(e)); - } - } - - private getCommonEventInfo(e: TPointerEventInfo) { - if (!this.tempReferenceLine || !e.absolutePointer) return; - return { - e: e.e, - transform: this.tempReferenceLine.get('transform'), - pointer: { - x: e.absolutePointer.x, - y: e.absolutePointer.y, - }, - target: this.tempReferenceLine, - }; - } - - private mouseUp(e: TPointerEventInfo) { - if (this.activeOn !== 'down') return; - this.canvas.selection = true - this.tempReferenceLine!.selectable = false - this.canvas.renderAll() - this.activeOn = 'up'; - // @ts-ignore - this.tempReferenceLine?.fire('up', this.getCommonEventInfo(e)); - this.tempReferenceLine = undefined; - } - - public setWorkSpaceDraw() { - this.workSpaceDraw = this.canvas.getObjects().filter(item => item.id === WorkSpaceDrawType)[0] as fabricRect - } - - public isRectOut(object: FabricObject, target: ReferenceLine): boolean { - // const { top, height, left, width } = object; - - // if (top === undefined || height === undefined || left === undefined || width === undefined) { - // return false; - // } - - // const targetRect = target.getBoundingRect(true, true); - // const { - // top: targetTop, - // height: targetHeight, - // left: targetLeft, - // width: targetWidth, - // } = targetRect; - - // if (target.isHorizontal() && (top > targetTop + 1 || top + height < targetTop + targetHeight - 1)) { - // return true; - // } - // else if (!target.isHorizontal() && (left > targetLeft + 1 || left + width < targetLeft + targetWidth - 1)) { - // return true; - // } - - return false; - }; - - referenceLineMoving(e: any) { - if (!this.workSpaceDraw) { - this.setWorkSpaceDraw(); - return; - } - const { target } = e; - if (this.isRectOut(this.workSpaceDraw, target)) { - target.moveCursor = 'not-allowed'; - } - } - - referenceLineMouseup(e: any) { - if (!this.workSpaceDraw) { - this.setWorkSpaceDraw(); - return; - } - const { target } = e; - if (this.isRectOut(this.workSpaceDraw, target)) { - this.canvas.remove(target); - this.canvas.setCursor(this.canvas.defaultCursor ?? ''); - } - } - - public get enabled() { - return this.options.enabled - } - - public set enabled(value) { - this.options.enabled = value - if (value) { - this.canvas.on(this.canvasEvents) - this.render({ ctx: this.canvas.contextContainer }) - } - else { - this.canvas.off(this.canvasEvents) - this.canvas.requestRenderAll() - } - } - - /** - * 获取画板尺寸 - */ - private getSize() { - return { - width: this.canvas.width, - height: this.canvas.height, - } - } - - private render({ ctx }: { ctx: CanvasRenderingContext2D }) { - if (ctx !== this.canvas.contextContainer) return - - const { viewportTransform: vpt } = this.canvas - - // 计算元素矩形 - this.calcObjectRect() - - // 绘制尺子 - this.draw({ - ctx, - isHorizontal: true, - rulerLength: this.getSize().width, - startCalibration: -(vpt[4] / vpt[0]), - }) - this.draw({ - ctx, - isHorizontal: false, - rulerLength: this.getSize().height, - startCalibration: -(vpt[5] / vpt[3]), - }) - - const { borderColor, backgroundColor, ruleSize, textColor } = this.options - - this.darwRect(ctx, { - left: 0, - top: 0, - width: ruleSize, - height: ruleSize, - fill: backgroundColor, - stroke: borderColor, - }) - - this.darwText(ctx, { - text: this.options.unitName, - left: ruleSize / 2, - top: ruleSize / 2, - align: 'center', - baseline: 'middle', - fill: textColor, - }) - } - - private draw(opt: {ctx: CanvasRenderingContext2D, isHorizontal: boolean, rulerLength: number, startCalibration: number}) { - const { ctx, isHorizontal, rulerLength, startCalibration } = opt - const zoom = this.canvas.getZoom() - - const gap = this.getGap(zoom) - const unitLength = Math.ceil(rulerLength / zoom) - const startValue = Math.floor(startCalibration / gap) * gap - const startOffset = startValue - startCalibration - - const canvasSize = this.getSize() - - const { textColor, borderColor, ruleSize, highlightColor } = this.options - - // 文字顶部偏移 - const padding = 2.5 - - // 背景 - this.darwRect(ctx, { - left: 0, - top: 0, - width: isHorizontal ? canvasSize.width : ruleSize, - height: isHorizontal ? ruleSize : canvasSize.height, - fill: this.options.backgroundColor, - stroke: this.options.borderColor, - }) - - // 标尺刻度线显示 - for (let pos = 0; pos + startOffset <= unitLength; pos += gap) { - for (let index = 0; index < 10; index++) { - const position = Math.round((startOffset + pos + (gap * index) / 10) * zoom) - const isMajorLine = index === 0 - const [left, top] = isHorizontal ? [position, isMajorLine ? 0 : ruleSize - 8] : [isMajorLine ? 0 : ruleSize - 8, position] - const [width, height] = isHorizontal ? [0, ruleSize - top] : [ruleSize - left, 0] - this.darwLine(ctx, { - left, - top, - width, - height, - stroke: borderColor, - }) - } - } - - // 标尺蓝色遮罩 - if (this.objectRect) { - const axis = isHorizontal ? 'x' : 'y' - this.objectRect[axis].forEach((rect) => { - // 跳过指定矩形 - if (rect.skip === axis) return - - const [left, top, width, height] = isHorizontal ? [(rect.left - startCalibration) * zoom, 0, rect.width * zoom, ruleSize] : [0, (rect.top - startCalibration) * zoom, ruleSize, rect.height * zoom] - - // 高亮遮罩 - // ctx.save() - this.darwRect(ctx, { - left, - top, - width, - height, - fill: highlightColor, - }) - // ctx.restore() - }) - } - - // 标尺文字显示 - for (let pos = 0; pos + startOffset <= unitLength; pos += gap) { - const position = (startOffset + pos) * zoom - let textValue = (startValue + pos).toString() - if (this.options.unitName === 'mm') { - textValue = px2mm(startValue + pos).toFixed(0) - } - const [left, top, angle] = isHorizontal ? [position + 6, padding, 0] : [padding, position - 6, -90] - - this.darwText(ctx, { - text: textValue, - left, - top, - fill: textColor, - angle, - }) - } - // draw end - } - - private getGap(zoom: number) { - const zooms = [0.02, 0.03, 0.05, 0.1, 0.2, 0.5, 1, 2, 5] - const gaps = [5000, 2500, 1000, 500, 200, 100, 50, 20, 10] - - let i = 0 - while (i < zooms.length && zooms[i] < zoom) { - i++ - } - - return gaps[i - 1] || 10000 - } - - private darwRect( - ctx: CanvasRenderingContext2D, - { - left, - top, - width, - height, - fill, - stroke, - strokeWidth, - }: { - left: number - top: number - width: number - height: number - fill?: string | CanvasGradient | CanvasPattern - stroke?: string - strokeWidth?: number - }, - ) { - ctx.save() - ctx.beginPath() - fill && (ctx.fillStyle = fill) - ctx.rect(left, top, width, height) - ctx.fill() - if (stroke) { - ctx.strokeStyle = stroke - ctx.lineWidth = strokeWidth ?? 1 - ctx.stroke() - } - ctx.restore() - } - - private darwText( - ctx: CanvasRenderingContext2D, - { - left, - top, - text, - fill, - align, - angle, - fontSize, - baseline, - }: { - left: number - top: number - text: string - fill?: string | CanvasGradient | CanvasPattern - align?: CanvasTextAlign - baseline?: CanvasTextBaseline - angle?: number - fontSize?: number - }, - ) { - ctx.save() - fill && (ctx.fillStyle = fill) - ctx.textAlign = align ?? 'left' - ctx.textBaseline = baseline ?? 'top' - ctx.font = `${fontSize ?? 12}px Helvetica` - if (angle) { - ctx.translate(left, top) - ctx.rotate(PiBy180 * angle) - ctx.translate(-left, -top) - } - ctx.fillText(text, left, top) - ctx.restore() - } - - private darwLine( - ctx: CanvasRenderingContext2D, - { - left, - top, - width, - height, - stroke, - lineWidth, - }: { - left: number - top: number - width: number - height: number - stroke?: string | CanvasGradient | CanvasPattern - lineWidth?: number - }, - ) { - ctx.save() - ctx.beginPath() - stroke && (ctx.strokeStyle = stroke) - ctx.lineWidth = lineWidth ?? 1 - ctx.moveTo(left, top) - ctx.lineTo(left + width, top + height) - ctx.stroke() - ctx.restore() - } - - private calcObjectRect() { - const activeObjects = this.canvas.getActiveObjects() - if (activeObjects.length === 0) { - this.objectRect = undefined - return - } - if (activeObjects[0].name.toLowerCase() === ElementNames.REFERENCELINE) { - this.objectRect = undefined - return - } - const allRect = activeObjects.reduce((rects, obj) => { - const rect: HighlightRect = obj.getBoundingRect(true) - rects.push(rect) - return rects - }, [] as HighlightRect[]) - if (allRect.length === 0) return - this.objectRect = { - x: this.mergeLines(allRect, true), - y: this.mergeLines(allRect, false), - } - } - - private mergeLines(rect: Rect[], isHorizontal: boolean) { - const axis = isHorizontal ? 'left' : 'top' - const length = isHorizontal ? 'width' : 'height' - // 先按照 axis 的大小排序 - rect.sort((a, b) => a[axis] - b[axis]) - const mergedLines = [] - let currentLine = Object.assign({}, rect[0]) - for (let i = 1; i < rect.length; i++) { - const line = Object.assign({}, rect[i]) - if (currentLine[axis] + currentLine[length] >= line[axis]) { - // 当前线段和下一个线段相交,合并宽度 - currentLine[length] = - Math.max(currentLine[axis] + currentLine[length], line[axis] + line[length]) - - currentLine[axis] - } else { - // 当前线段和下一个线段不相交,将当前线段加入结果数组中,并更新当前线段为下一个线段 - mergedLines.push(currentLine) - currentLine = Object.assign({}, line) - } - } - // 加入数组 - mergedLines.push(currentLine) - return mergedLines - } - - public dispose(): void { - super.dispose() - this.enabled = false - } -} diff --git a/src/app/fabricTool.ts b/src/app/fabricTool.ts deleted file mode 100644 index 687220f..0000000 --- a/src/app/fabricTool.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { Point, Canvas, Object as FabricObject } from 'fabric' -import { watch, computed } from 'vue' -import { storeToRefs } from 'pinia' -import { Disposable } from '@/utils/lifecycle' -import useCanvasSwipe from '@/hooks/useCanvasSwipe' -import { useKeyboardStore } from '@/store' -import { useActiveElement, toValue } from '@vueuse/core' - - -type ToolOption = { - defaultCursor: string - skipTargetFind: boolean - selection: boolean -} - -type ToolType = 'move' | 'handMove' | 'shape' - - -export class FabricTool extends Disposable { - - private options: Record = { - move: { - defaultCursor: 'default', - skipTargetFind: false, - selection: true, - }, - handMove: { - defaultCursor: 'grab', - skipTargetFind: true, - selection: false, - }, - shape: { - defaultCursor: 'crosshair', - skipTargetFind: true, - selection: false, - }, - } - - private _handMoveActivate = false - - private get handMoveActivate() { - return this._handMoveActivate - } - - private set handMoveActivate(value) { - this._handMoveActivate = value - } - - constructor(private readonly canvas: Canvas) { - super() - this.initHandMove() - } - - private applyOption(tool: ToolType) { - const { defaultCursor, skipTargetFind, selection } = this.options[tool] - - this.canvas.defaultCursor = defaultCursor - this.canvas.setCursor(defaultCursor) - this.canvas.skipTargetFind = skipTargetFind - this.canvas.selection = selection - } - - // private switchShape(shape: 'board' | 'rect' | 'ellipse' | 'triangle' | 'text') { - // const { canvas } = this - // let coordsStart: Point | undefined - // let tempObject: FabricObject | undefined - // const { stop, isSwiping } = useFabricSwipe({ - // onSwipeStart: (e) => { - // if (e.button !== 1 || this.space.value) return - // /* - // * 只有mouseMove的时候isSwiping才会为true - // * mouseUp会判断isSwiping的值来决定是否执行onSwipeEnd - // * 这里强制设置成true,让点击也可执行onSwipeEnd - // */ - // isSwiping.value = true - // // 获得坐标 - // coordsStart = e.pointer - // // 创建形状 - // switch (shape) { - // case 'board': - // tempObject = new Board([], { - // fill: '', - // }) - // break - // case 'rect': - // tempObject = new Rect({}) - // break - // case 'ellipse': - // tempObject = new Ellipse({ - // rx: 50, - // ry: 50, - // }) - // break - // case 'triangle': - // tempObject = new Triangle({}) - // break - // case 'text': - // tempObject = new Textbox('', {}) - // break - // } - // tempObject.set({ - // left: coordsStart.x, - // top: coordsStart.y, - // width: 100, - // height: 100, - // scaleX: 0.01, - // scaleY: 0.01, - // hideOnLayer: true, - // }) - // // 不发送ObjectAdded事件 - // tempObject.noEventObjectAdded = true - // // 添加对象到画板 - // const board = this.canvas._searchPossibleTargets( - // this.canvas.getObjects('Board'), - // e.absolutePointer, - // ) as Board | undefined - // const parent = board || canvas - // parent.add(tempObject) - // // 取消不发送 - // tempObject.noEventObjectAdded = false - // // 设置激活对象 - // canvas.setActiveObject(tempObject) - // tempObject.__corner = 'br' - // canvas._setupCurrentTransform(e.e, tempObject, true) - // }, - // onSwipeEnd: (e) => { - // if (!tempObject) return - // console.log('onSwipeEnd:', tempObject) - // // 如果点击画板,没有移动,设置默认宽高 - // if (tempObject.scaleX <= 0.01 && tempObject.scaleY <= 0.01) { - // tempObject.set({ - // left: tempObject.left - 50, - // top: tempObject.top - 50, - // scaleX: 1, - // scaleY: 1, - // }) - // } - // // 设置宽高缩放 - // tempObject.set({ - // width: tempObject.getScaledWidth(), - // height: tempObject.getScaledHeight(), - // scaleX: 1, - // scaleY: 1, - // hideOnLayer: false, - // }) - // // 特殊形状处理 - // if (tempObject instanceof Board) { - // tempObject.set({ - // fill: '#ffffff', - // }) - // } else if (tempObject instanceof Ellipse) { - // tempObject.set({ - // rx: tempObject.width / 2, - // ry: tempObject.height / 2, - // }) - // } else if (tempObject instanceof Textbox) { - // tempObject.set({ - // text: '输入文本', - // }) - // canvas.defaultCursor = 'default' - // tempObject.enterEditing(e.e) - // tempObject.selectAll() - // } - // // 通知事件 - // if (!tempObject.group) { - // canvas._onObjectAdded(tempObject) - // } - // canvas.fire('selection:updated') - // canvas.requestRenderAll() - // tempObject = undefined - // useAppStore().activeTool = 'move' - // }, - // }) - // this.toolStop = stop - // } - - - /** - *鼠标中键拖动视窗 - */ - private initHandMove() { - const canvas = this.canvas - - /** 鼠标移动开始的vpt */ - let vpt = canvas.viewportTransform - const { spaceKeyState } = storeToRefs(useKeyboardStore()) - const { lengthX, lengthY, isSwiping } = useCanvasSwipe({ - onSwipeStart: (e) => { - - if (e.button === 2 || (spaceKeyState.value && e.button === 1)) { - isSwiping.value = true - vpt = canvas.viewportTransform - this.handMoveActivate = true - // this.applyOption('handMove') - // canvas.setCursor('grab') - } - }, - onSwipe: () => { - if (!this.handMoveActivate) return - - canvas.setCursor('grab') - - requestAnimationFrame(() => { - const deltaPoint = new Point(lengthX.value, lengthY.value).scalarDivide(canvas.getZoom()).transform(vpt).scalarMultiply(-1) - canvas.absolutePan(deltaPoint) - }) - }, - onSwipeEnd: () => { - // 恢复鼠标指针 - this.applyOption(spaceKeyState.value ? 'handMove' : 'move') - if (!this.handMoveActivate) return - // 关闭 handMove - if (!spaceKeyState.value) { - this.handMoveActivate = false - } - }, - }) - - // 空格键切换移动工具 - const activeElement = useActiveElement() - const activeElementHasInput = computed(() => activeElement.value?.tagName !== 'INPUT' && activeElement.value?.tagName !== 'TEXTAREA') - - - watch( - computed(() => [spaceKeyState.value, activeElementHasInput.value].every((i) => toValue(i))), - (space) => { - this.applyOption(space ? 'handMove' : 'move') - if (isSwiping.value) return - this.handMoveActivate = space - }, - ) - } -} diff --git a/src/app/hoverBorders.ts b/src/app/hoverBorders.ts deleted file mode 100644 index 6903760..0000000 --- a/src/app/hoverBorders.ts +++ /dev/null @@ -1,172 +0,0 @@ - -import { Object as FabricObject, CanvasEvents, Canvas, Rect, Textbox, IText } from 'fabric' -import { clone } from 'lodash-es' -import { check } from '@/utils/check' -import { Disposable } from '@/utils/lifecycle' -import { addDisposableListener } from '@/utils/dom' -import { useMainStore } from '@/store' -import { storeToRefs } from 'pinia' -import { computed, watch } from 'vue' - -/** - * 对象获得焦点后在外围显示一个边框 - */ -export class HoverBorders extends Disposable { - private canvasEvents - - private lineWidth = 2 - private hoveredTarget: FabricObject | undefined - - constructor(private readonly canvas: Canvas) { - super() - - this.canvasEvents = { - 'mouse:out': this.drawBorder.bind(this), - 'mouse:over': this.clearBorder.bind(this), - } - - canvas.on(this.canvasEvents) - - this._register( - addDisposableListener(this.canvas.upperCanvasEl, 'mouseout', () => { - if (this.canvas.contextTopDirty && this.hoveredTarget) { - this.clearContextTop(this.hoveredTarget.group || this.hoveredTarget) - this.hoveredTarget = undefined - } - }), - ) - this.initWatch() - } - - private clearContextTop(target: FabricObject, restoreManually = false) { - const ctx = this.canvas.contextTop - ctx.save() - ctx.transform(...this.canvas.viewportTransform) - target.transform(ctx) - const { strokeWidth, scaleX, scaleY, strokeUniform } = target - const zoom = this.canvas.getZoom() - // we add 4 pixel, to be sure to do not leave any pixel out - const width = target.width + 4 / zoom + (strokeUniform ? strokeWidth / scaleX : strokeWidth) - const height = target.height + 4 / zoom + (strokeUniform ? strokeWidth / scaleY : strokeWidth) - ctx.clearRect(-width / 2, -height / 2, width, height) - restoreManually || ctx.restore() - return ctx - } - - private clearBorder(e: CanvasEvents['mouse:over']) { - const target = e.target - - this.hoveredTarget = undefined - - if (!target || target === this.canvas._activeObject) return - - this.clearBorderByObject(target) - } - - private clearBorderByObject(target: FabricObject) { - - if (this.canvas.contextTopDirty) { - this.clearContextTop(target) - } - } - - private drawBorder(e: CanvasEvents['mouse:out']) { - const target = e.target - - if (!target || target === this.canvas._activeObject) return - - this.drawBorderByObject(target) - } - - private drawBorderByObject(target: FabricObject) { - - this.hoveredTarget = target - - const ctx = this.clearContextTop(target, true) - if (!ctx) return - - const object = clone(target) - - // 文字特殊处理,显示下划线 - if (object instanceof Textbox && object.isType('Textbox')) { - this.showUnderline(ctx, object as Textbox) - return - } - if (object instanceof IText && object.isType('IText')) { - this.showUnderline(ctx, object as Textbox) - return - } - // 分组特殊处理,显示矩形边框 - if (check.isCollection(object) || object.isType('ArcText')) { - object._render = Rect.prototype._render - } - - const { strokeWidth, strokeUniform } = object - - let { width, height } = object - - width += strokeUniform ? strokeWidth / object.scaleX : strokeWidth - height += strokeUniform ? strokeWidth / object.scaleY : strokeWidth - - const totalObjectScaling = object.getTotalObjectScaling() - - const lineWidth = Math.min( - this.lineWidth, - width * totalObjectScaling.x, - height * totalObjectScaling.y, - ) - - width -= lineWidth / totalObjectScaling.x - height -= lineWidth / totalObjectScaling.y - - object.set({ - width, - height, - stroke: 'rgb(60,126,255)', - strokeWidth: lineWidth, - strokeDashArray: null, - strokeDashOffset: 0, - strokeLineCap: 'butt', - strokeLineJoin: 'miter', - strokeMiterLimit: 4, - }) - - object._renderPaintInOrder = () => { - ctx.save() - const scaling = object.getTotalObjectScaling() - ctx.scale(1 / scaling.x, 1 / scaling.y) - object._setLineDash(ctx, object.strokeDashArray) - object._setStrokeStyles(ctx, object) - ctx.stroke() - ctx.restore() - } - - object._render(ctx) - - ctx.restore() - this.canvas.contextTopDirty = true - } - - public showUnderline(ctx: CanvasRenderingContext2D, object: Textbox) { - object.underline = true - object.fill = 'rgb(60,126,255)' - object._renderTextDecoration(ctx, 'underline') - object._drawClipPath(ctx, object.clipPath) - ctx.restore() - this.canvas.contextTopDirty = true - } - - public initWatch() { - const mainStore = useMainStore() - const { hoveredObject, leavedObject } = storeToRefs(mainStore) - computed(() => { - if (hoveredObject.value) this.drawBorderByObject(hoveredObject.value as FabricObject) - else this.clearBorderByObject(leavedObject.value as FabricObject) - }) - } - - public dispose(): void { - super.dispose() - this.canvas.off(this.canvasEvents) - } -} diff --git a/src/app/instantiation/descriptors.ts b/src/app/instantiation/descriptors.ts deleted file mode 100644 index c831bc2..0000000 --- a/src/app/instantiation/descriptors.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export class SyncDescriptor { - readonly ctor: any - readonly staticArguments: any[] - readonly supportsDelayedInstantiation: boolean - - constructor( - ctor: new (...args: any[]) => T, - staticArguments: any[] = [], - supportsDelayedInstantiation = false, - ) { - this.ctor = ctor - this.staticArguments = staticArguments - this.supportsDelayedInstantiation = supportsDelayedInstantiation - } -} - -export interface SyncDescriptor0 { - readonly ctor: new () => T -} diff --git a/src/app/instantiation/extensions.ts b/src/app/instantiation/extensions.ts deleted file mode 100644 index 5c24ebb..0000000 --- a/src/app/instantiation/extensions.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { SyncDescriptor } from './descriptors' -import { BrandedService, ServiceIdentifier } from './instantiation' - -const _registry: [ServiceIdentifier, SyncDescriptor][] = [] - -export const enum InstantiationType { - /** - * Instantiate this service as soon as a consumer depends on it. _Note_ that this - * is more costly as some upfront work is done that is likely not needed - */ - Eager = 0, - - /** - * Instantiate this service as soon as a consumer uses it. This is the _better_ - * way of registering a service. - */ - Delayed = 1, -} - -export function registerSingleton( - id: ServiceIdentifier, - ctor: new (...services: Services) => T, - supportsDelayedInstantiation: InstantiationType, -): void -export function registerSingleton( - id: ServiceIdentifier, - descriptor: SyncDescriptor, -): void -export function registerSingleton( - id: ServiceIdentifier, - ctorOrDescriptor: { new (...services: Services): T } | SyncDescriptor, - supportsDelayedInstantiation?: boolean | InstantiationType, -): void { - if (!(ctorOrDescriptor instanceof SyncDescriptor)) { - ctorOrDescriptor = new SyncDescriptor( - ctorOrDescriptor as new (...args: any[]) => T, - [], - Boolean(supportsDelayedInstantiation), - ) - } - - _registry.push([id, ctorOrDescriptor]) -} - -export function getSingletonServiceDescriptors(): [ServiceIdentifier, SyncDescriptor][] { - return _registry -} diff --git a/src/app/instantiation/graph.ts b/src/app/instantiation/graph.ts deleted file mode 100644 index 4be233c..0000000 --- a/src/app/instantiation/graph.ts +++ /dev/null @@ -1,108 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export class Node { - readonly incoming = new Map>() - readonly outgoing = new Map>() - - constructor(readonly key: string, readonly data: T) {} -} - -export class Graph { - private readonly _nodes = new Map>() - - constructor(private readonly _hashFn: (element: T) => string) { - // empty - } - - roots(): Node[] { - const ret: Node[] = [] - for (const node of this._nodes.values()) { - if (node.outgoing.size === 0) { - ret.push(node) - } - } - return ret - } - - insertEdge(from: T, to: T): void { - const fromNode = this.lookupOrInsertNode(from) - const toNode = this.lookupOrInsertNode(to) - - fromNode.outgoing.set(toNode.key, toNode) - toNode.incoming.set(fromNode.key, fromNode) - } - - removeNode(data: T): void { - const key = this._hashFn(data) - this._nodes.delete(key) - for (const node of this._nodes.values()) { - node.outgoing.delete(key) - node.incoming.delete(key) - } - } - - lookupOrInsertNode(data: T): Node { - const key = this._hashFn(data) - let node = this._nodes.get(key) - - if (!node) { - node = new Node(key, data) - this._nodes.set(key, node) - } - - return node - } - - lookup(data: T): Node | undefined { - return this._nodes.get(this._hashFn(data)) - } - - isEmpty(): boolean { - return this._nodes.size === 0 - } - - toString(): string { - const data: string[] = [] - for (const [key, value] of this._nodes) { - data.push( - `${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[ - ...value.outgoing.keys(), - ].join(',')}]\n`, - ) - } - return data.join('\n') - } - - /** - * This is brute force and slow and **only** be used - * to trouble shoot. - */ - findCycleSlow() { - for (const [id, node] of this._nodes) { - const seen = new Set([id]) - const res = this._findCycle(node, seen) - if (res) { - return res - } - } - return undefined - } - - private _findCycle(node: Node, seen: Set): string | undefined { - for (const [id, outgoing] of node.outgoing) { - if (seen.has(id)) { - return [...seen, id].join(' -> ') - } - seen.add(id) - const value = this._findCycle(outgoing, seen) - if (value) { - return value - } - seen.delete(id) - } - return undefined - } -} diff --git a/src/app/instantiation/instantiation.ts b/src/app/instantiation/instantiation.ts deleted file mode 100644 index e81ca15..0000000 --- a/src/app/instantiation/instantiation.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as descriptors from './descriptors' -import { ServiceCollection } from './serviceCollection' - -// ------ internal util -export const _util = { - serviceIds: new Map>(), - DI_TARGET: '$di$target', - DI_DEPENDENCIES: '$di$dependencies', - getServiceDependencies(ctor: any): { id: ServiceIdentifier; index: number }[] { - return ctor[this.DI_DEPENDENCIES] || [] - }, -} - -// --- interfaces ------ - -export type BrandedService = { _serviceBrand: undefined } - -export interface IConstructorSignature { - new (...args: [...Args, ...Services]): T -} - -export interface ServicesAccessor { - get(id: ServiceIdentifier): T -} - -export const IInstantiationService = createDecorator('instantiationService') - -/** - * Given a list of arguments as a tuple, attempt to extract the leading, non-service arguments - * to their own tuple. - */ -export type GetLeadingNonServiceArgs = TArgs extends [] - ? [] - : TArgs extends [...infer TFirst, BrandedService] - ? GetLeadingNonServiceArgs - : TArgs - -export interface IInstantiationService { - readonly _serviceBrand: undefined - - /** - * Synchronously creates an instance that is denoted by the descriptor - */ - createInstance(descriptor: descriptors.SyncDescriptor0): T - createInstance any, R extends InstanceType>( - ctor: Ctor, - ...args: GetLeadingNonServiceArgs> - ): R - - /** - * Calls a function with a service accessor. - */ - invokeFunction( - fn: (accessor: ServicesAccessor, ...args: TS) => R, - ...args: TS - ): R - - /** - * Creates a child of this service which inherits all current services - * and adds/overwrites the given services. - */ - createChild(services: ServiceCollection): IInstantiationService -} - -/** - * Identifies a service of type `T`. - */ -export interface ServiceIdentifier { - (...args: any[]): void - type: T -} - -function storeServiceDependency(id: Function, target: Function, index: number): void { - if ((target as any)[_util.DI_TARGET] === target) { - ;(target as any)[_util.DI_DEPENDENCIES].push({ id, index }) - } else { - ;(target as any)[_util.DI_DEPENDENCIES] = [{ id, index }] - ;(target as any)[_util.DI_TARGET] = target - } -} - -/** - * The *only* valid way to create a {{ServiceIdentifier}}. - */ -export function createDecorator(serviceId: string): ServiceIdentifier { - if (_util.serviceIds.has(serviceId)) { - return _util.serviceIds.get(serviceId)! - } - - const id = function (target: Function, key: string, index: number): any { - if (arguments.length !== 3) { - throw new Error('@IServiceName-decorator can only be used to decorate a parameter') - } - storeServiceDependency(id, target, index) - } - - id.toString = () => serviceId - - _util.serviceIds.set(serviceId, id) - return id -} diff --git a/src/app/instantiation/instantiationService.ts b/src/app/instantiation/instantiationService.ts deleted file mode 100644 index 9abf426..0000000 --- a/src/app/instantiation/instantiationService.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { SyncDescriptor, SyncDescriptor0 } from './descriptors' -import { Graph } from './graph' -import { - GetLeadingNonServiceArgs, - IInstantiationService, - ServiceIdentifier, - ServicesAccessor, - _util, -} from './instantiation' -import { ServiceCollection } from './serviceCollection' -import { IdleValue } from '@/utils/async' -import { LinkedList } from '@/utils/linkedList' -import { toDisposable, IDisposable, DisposableStore } from '@/utils/lifecycle' - -interface Event { - ( - listener: (e: T) => any, - thisArgs?: any, - disposables?: IDisposable[] | DisposableStore, - ): IDisposable -} - -// TRACING -const _enableAllTracing = false -// || "TRUE" // DO NOT CHECK IN! - -class CyclicDependencyError extends Error { - constructor(graph: Graph) { - super('cyclic dependency between services') - this.message = - graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}` - } -} - -export class InstantiationService implements IInstantiationService { - declare readonly _serviceBrand: undefined - - readonly _globalGraph?: Graph - private _globalGraphImplicitDependency?: string - - constructor( - private readonly _services: ServiceCollection = new ServiceCollection(), - private readonly _strict: boolean = false, - private readonly _parent?: InstantiationService, - private readonly _enableTracing: boolean = _enableAllTracing, - ) { - this._services.set(IInstantiationService, this) - this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph((e) => e) : undefined - } - - createChild(services: ServiceCollection): IInstantiationService { - return new InstantiationService(services, this._strict, this, this._enableTracing) - } - - invokeFunction( - fn: (accessor: ServicesAccessor, ...args: TS) => R, - ...args: TS - ): R { - const _trace = Trace.traceInvocation(this._enableTracing, fn) - let _done = false - try { - const accessor: ServicesAccessor = { - get: (id: ServiceIdentifier) => { - if (_done) { - throw new Error( - 'service accessor is only valid during the invocation of its target method', - ) - } - - const result = this._getOrCreateServiceInstance(id, _trace) - if (!result) { - throw new Error(`[invokeFunction] unknown service '${id}'`) - } - return result - }, - } - return fn(accessor, ...args) - } finally { - _done = true - _trace.stop() - } - } - - createInstance(descriptor: SyncDescriptor0): T - createInstance any, R extends InstanceType>( - ctor: Ctor, - ...args: GetLeadingNonServiceArgs> - ): R - createInstance(ctorOrDescriptor: any | SyncDescriptor, ...rest: any[]): any { - let _trace: Trace - let result: any - if (ctorOrDescriptor instanceof SyncDescriptor) { - _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor) - result = this._createInstance( - ctorOrDescriptor.ctor, - ctorOrDescriptor.staticArguments.concat(rest), - _trace, - ) - } else { - _trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor) - result = this._createInstance(ctorOrDescriptor, rest, _trace) - } - _trace.stop() - return result - } - - private _createInstance(ctor: any, args: any[] = [], _trace: Trace): T { - // arguments defined by service decorators - const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index) - const serviceArgs: any[] = [] - for (const dependency of serviceDependencies) { - const service = this._getOrCreateServiceInstance(dependency.id, _trace) - if (!service) { - this._throwIfStrict( - `[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, - false, - ) - } - serviceArgs.push(service) - } - - const firstServiceArgPos = - serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length - - // check for argument mismatches, adjust static args if needed - if (args.length !== firstServiceArgPos) { - console.trace( - `[createInstance] First service dependency of ${ctor.name} at position ${ - firstServiceArgPos + 1 - } conflicts with ${args.length} static arguments`, - ) - - const delta = firstServiceArgPos - args.length - if (delta > 0) { - args = args.concat(new Array(delta)) - } else { - args = args.slice(0, firstServiceArgPos) - } - } - - // now create the instance - return Reflect.construct(ctor, args.concat(serviceArgs)) - } - - private _setServiceInstance(id: ServiceIdentifier, instance: T): void { - if (this._services.get(id) instanceof SyncDescriptor) { - this._services.set(id, instance) - } else if (this._parent) { - this._parent._setServiceInstance(id, instance) - } else { - throw new Error('illegalState - setting UNKNOWN service instance') - } - } - - private _getServiceInstanceOrDescriptor(id: ServiceIdentifier): T | SyncDescriptor { - const instanceOrDesc = this._services.get(id) - if (!instanceOrDesc && this._parent) { - return this._parent._getServiceInstanceOrDescriptor(id) - } else { - return instanceOrDesc - } - } - - protected _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { - if (this._globalGraph && this._globalGraphImplicitDependency) { - this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id)) - } - const thing = this._getServiceInstanceOrDescriptor(id) - if (thing instanceof SyncDescriptor) { - return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)) - } else { - _trace.branch(id, false) - return thing - } - } - - private readonly _activeInstantiations = new Set>() - - private _safeCreateAndCacheServiceInstance( - id: ServiceIdentifier, - desc: SyncDescriptor, - _trace: Trace, - ): T { - if (this._activeInstantiations.has(id)) { - throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`) - } - this._activeInstantiations.add(id) - try { - return this._createAndCacheServiceInstance(id, desc, _trace) - } finally { - this._activeInstantiations.delete(id) - } - } - - private _createAndCacheServiceInstance( - id: ServiceIdentifier, - desc: SyncDescriptor, - _trace: Trace, - ): T { - type Triple = { id: ServiceIdentifier; desc: SyncDescriptor; _trace: Trace } - const graph = new Graph((data) => data.id.toString()) - - let cycleCount = 0 - const stack = [{ id, desc, _trace }] - while (stack.length) { - const item = stack.pop()! - graph.lookupOrInsertNode(item) - - // a weak but working heuristic for cycle checks - if (cycleCount++ > 1000) { - throw new CyclicDependencyError(graph) - } - - // check all dependencies for existence and if they need to be created first - for (const dependency of _util.getServiceDependencies(item.desc.ctor)) { - const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id) - if (!instanceOrDesc) { - this._throwIfStrict( - `[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, - true, - ) - } - - // take note of all service dependencies - this._globalGraph?.insertEdge(String(item.id), String(dependency.id)) - - if (instanceOrDesc instanceof SyncDescriptor) { - const d = { - id: dependency.id, - desc: instanceOrDesc, - _trace: item._trace.branch(dependency.id, true), - } - graph.insertEdge(item, d) - stack.push(d) - } - } - } - - // eslint-disable-next-line no-constant-condition - while (true) { - const roots = graph.roots() - - // if there is no more roots but still - // nodes in the graph we have a cycle - if (roots.length === 0) { - if (!graph.isEmpty()) { - throw new CyclicDependencyError(graph) - } - break - } - - for (const { data } of roots) { - // Repeat the check for this still being a service sync descriptor. That's because - // instantiating a dependency might have side-effect and recursively trigger instantiation - // so that some dependencies are now fullfilled already. - const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id) - if (instanceOrDesc instanceof SyncDescriptor) { - // create instance and overwrite the service collections - const instance = this._createServiceInstanceWithOwner( - data.id, - data.desc.ctor, - data.desc.staticArguments, - data.desc.supportsDelayedInstantiation, - data._trace, - ) - this._setServiceInstance(data.id, instance) - } - graph.removeNode(data) - } - } - return this._getServiceInstanceOrDescriptor(id) - } - - private _createServiceInstanceWithOwner( - id: ServiceIdentifier, - ctor: any, - args: any[] = [], - supportsDelayedInstantiation: boolean, - _trace: Trace, - ): T { - if (this._services.get(id) instanceof SyncDescriptor) { - return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace) - } else if (this._parent) { - return this._parent._createServiceInstanceWithOwner( - id, - ctor, - args, - supportsDelayedInstantiation, - _trace, - ) - } else { - throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`) - } - } - - private _createServiceInstance( - id: ServiceIdentifier, - ctor: any, - args: any[] = [], - supportsDelayedInstantiation: boolean, - _trace: Trace, - ): T { - if (!supportsDelayedInstantiation) { - // eager instantiation - return this._createInstance(ctor, args, _trace) - } else { - const child = new InstantiationService(undefined, this._strict, this, this._enableTracing) - child._globalGraphImplicitDependency = String(id) - - // Return a proxy object that's backed by an idle value. That - // strategy is to instantiate services in our idle time or when actually - // needed but not when injected into a consumer - - // return "empty events" when the service isn't instantiated yet - const earlyListeners = new Map>>>() - - const idle = new IdleValue(() => { - const result = child._createInstance(ctor, args, _trace) - - // early listeners that we kept are now being subscribed to - // the real service - for (const [key, values] of earlyListeners) { - const candidate = >(result)[key] - if (typeof candidate === 'function') { - for (const listener of values) { - candidate.apply(result, listener) - } - } - } - earlyListeners.clear() - - return result - }) - return new Proxy(Object.create(null), { - get(target: any, key: PropertyKey): any { - if (!idle.isInitialized) { - // looks like an event - if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) { - let list = earlyListeners.get(key) - if (!list) { - list = new LinkedList() - earlyListeners.set(key, list) - } - const event: Event = (callback, thisArg, disposables) => { - const rm = list!.push([callback, thisArg, disposables]) - return toDisposable(rm) - } - return event - } - } - - // value already exists - if (key in target) { - return target[key] - } - - // create value - const obj = idle.value - let prop = obj[key] - if (typeof prop !== 'function') { - return prop - } - prop = prop.bind(obj) - target[key] = prop - return prop - }, - set(_target: T, p: PropertyKey, value: any): boolean { - idle.value[p] = value - return true - }, - getPrototypeOf(_target: T) { - return ctor.prototype - }, - }) - } - } - - private _throwIfStrict(msg: string, printWarning: boolean): void { - if (printWarning) { - console.warn(msg) - } - if (this._strict) { - throw new Error(msg) - } - } -} - -//#region -- tracing --- - -const enum TraceType { - None = 0, - Creation = 1, - Invocation = 2, - Branch = 3, -} - -export class Trace { - static all = new Set() - - private static readonly _None = new (class extends Trace { - constructor() { - super(TraceType.None, null) - } - override stop() {} - override branch() { - return this - } - })() - - static traceInvocation(_enableTracing: boolean, ctor: any): Trace { - return !_enableTracing - ? Trace._None - : new Trace( - TraceType.Invocation, - ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n'), - ) - } - - static traceCreation(_enableTracing: boolean, ctor: any): Trace { - return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name) - } - - private static _totals = 0 - private readonly _start: number = Date.now() - private readonly _dep: [ServiceIdentifier, boolean, Trace?][] = [] - - private constructor(readonly type: TraceType, readonly name: string | null) {} - - branch(id: ServiceIdentifier, first: boolean): Trace { - const child = new Trace(TraceType.Branch, id.toString()) - this._dep.push([id, first, child]) - return child - } - - stop() { - const dur = Date.now() - this._start - Trace._totals += dur - - let causedCreation = false - - function printChild(n: number, trace: Trace) { - const res: string[] = [] - const prefix = new Array(n + 1).join('\t') - for (const [id, first, child] of trace._dep) { - if (first && child) { - causedCreation = true - res.push(`${prefix}CREATES -> ${id}`) - const nested = printChild(n + 1, child) - if (nested) { - res.push(nested) - } - } else { - res.push(`${prefix}uses -> ${id}`) - } - } - return res.join('\n') - } - - const lines = [ - `${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`, - `${printChild(1, this)}`, - `DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`, - ] - - if (dur > 2 || causedCreation) { - Trace.all.add(lines.join('\n')) - } - } -} - -//#endregion diff --git a/src/app/instantiation/serviceCollection.ts b/src/app/instantiation/serviceCollection.ts deleted file mode 100644 index 904a845..0000000 --- a/src/app/instantiation/serviceCollection.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ServiceIdentifier } from './instantiation' -import { SyncDescriptor } from './descriptors' - -export class ServiceCollection { - private _entries = new Map, any>() - - constructor(...entries: [ServiceIdentifier, any][]) { - for (const [id, service] of entries) { - this.set(id, service) - } - } - - set( - id: ServiceIdentifier, - instanceOrDescriptor: T | SyncDescriptor, - ): T | SyncDescriptor { - const result = this._entries.get(id) - this._entries.set(id, instanceOrDescriptor) - return result - } - - has(id: ServiceIdentifier): boolean { - return this._entries.has(id) - } - - get(id: ServiceIdentifier): T | SyncDescriptor { - return this._entries.get(id) - } -} diff --git a/src/app/keybinding.ts b/src/app/keybinding.ts deleted file mode 100644 index 960866b..0000000 --- a/src/app/keybinding.ts +++ /dev/null @@ -1,63 +0,0 @@ -import mousetrap, { ExtendedKeyboardEvent } from 'mousetrap' -import { isArray, isFunction, isObject, isString } from 'lodash-es' -import { createDecorator } from './instantiation/instantiation' -import { registerSingleton, InstantiationType } from './instantiation/extensions' -import { runWhenIdle } from '@/utils/async' - -export const IKeybindingService = createDecorator('Keybinding') - -type Callback = (e: ExtendedKeyboardEvent, combo: string) => void - -export class Keybinding extends mousetrap { - declare readonly _serviceBrand: undefined - - public mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl' - - constructor() { - super() - } - - /** - * Overwrites default Mousetrap.bind method to optionally accept - * an object to bind multiple key events in a single call - * - * You can pass it in like: - * - * Mousetrap.bind({ - * 'a': function() { console.log('a'); }, - * 'b': function() { console.log('b'); } - * }); - * - * And can optionally pass in 'keypress', 'keydown', or 'keyup' - * as a second argument - * - */ - override bind(keys: string | string[], callback: Callback, action?: string): this - override bind(keys: { [key: string]: Callback }, action?: string): this - override bind( - keys: string | string[] | { [key: string]: Callback }, - callbackOrAction?: string | Callback, - action?: string, - ) { - if ((isString(keys) || isArray(keys)) && isFunction(callbackOrAction)) { - return super.bind(keys, callbackOrAction, action) - } - - if (isObject(keys) && !isArray(keys) && (!callbackOrAction || isString(callbackOrAction))) { - for (const key in keys) { - super.bind(key, keys[key], callbackOrAction) - } - } - - return this - } - - override trigger(keys: string, action?: string | undefined) { - runWhenIdle(() => { - super.trigger(keys, action) - }) - return this - } -} - -registerSingleton(IKeybindingService, Keybinding, InstantiationType.Eager) diff --git a/src/app/wheelScroll.ts b/src/app/wheelScroll.ts deleted file mode 100644 index 367978d..0000000 --- a/src/app/wheelScroll.ts +++ /dev/null @@ -1,155 +0,0 @@ - -import { CanvasEvents, Canvas, Point, TPointerEvent, TPointerEventInfo } from 'fabric' -import { useIntervalFn, useMagicKeys } from '@vueuse/core' -import { Disposable, toDisposable } from '@/utils/lifecycle' -import { debounce } from 'lodash-es' -import { storeToRefs } from 'pinia' -import { useFabricStore } from '@/store' - -/** - * 画板默认滚动行为 - */ -export class WheelScroll extends Disposable { - private edgeMoveStatus = true - - constructor(private readonly canvas: Canvas) { - super() - this.initWhellScroll() - this.initEdgeMove() - } - - /** - * 鼠标滚动 - */ - private initWhellScroll() { - const { ctrl, cmd, shift } = useMagicKeys() - const fabricStore = useFabricStore() - const { zoom } = storeToRefs(fabricStore) - const mouseWheel = (e: CanvasEvents['mouse:wheel']) => { - e.e.preventDefault() - e.e.stopPropagation() - const { deltaX, deltaY, offsetX, offsetY } = e.e - // 缩放视窗 - if (ctrl.value || cmd.value) { - const zoomFactor = Math.abs(deltaY) < 10 ? deltaY * 2 : deltaY / 3 - const canvasZoom = this.canvas.getZoom() - let zoomVal = canvasZoom * (1 - zoomFactor / 200) - if (zoomVal > 0.97 && zoomVal < 1.03) { - zoomVal = 1 - } - zoom.value = zoomVal - this.canvas.zoomToPoint(new Point(offsetX, offsetY), zoomVal) - this.setCoords() - return - } - - // 滚动画布 - const deltaPoint = new Point() - if (shift.value) { - deltaPoint.x = deltaY > 0 ? -20 : 20 - } else { - deltaPoint.y = deltaY > 0 ? -20 : 20 - } - this.canvas.relativePan(deltaPoint) - this.setCoords() - } - - this.canvas.on('mouse:wheel', mouseWheel) - this._register( - toDisposable(() => { - this.canvas.off('mouse:wheel', mouseWheel) - }), - ) - } - - /** - * 更新所有元素坐标 - */ - private setCoords = debounce(() => { - const { renderOnAddRemove } = this.canvas - this.canvas.renderOnAddRemove = false - this.canvas.setViewportTransform(this.canvas.viewportTransform) - this.canvas.renderOnAddRemove = renderOnAddRemove - }, 150) - - /** - * 边缘移动 - */ - private initEdgeMove() { - let event: TPointerEventInfo | undefined - - /** 是否需要执行setCoords */ - let needSetCoords = false - - const { pause, resume } = useIntervalFn(() => { - if (!event) return - - const A = new Point(24, 24) - const B = new Point(this.canvas.width, this.canvas.height).subtract(A) - const [pos, distance] = this.judgePosition(event.absolutePointer, A, B) - if (pos === 0) return - - let deltaPoint = new Point() - const amount = Math.min(distance, 20) - if (pos & 1) deltaPoint.x = amount - if (pos & 2) deltaPoint.x = -amount - if (pos & 4) deltaPoint.y = amount - if (pos & 8) deltaPoint.y = -amount - - // 移动到四个角落,减速 - if (deltaPoint.x !== 0 && deltaPoint.y !== 0) { - deltaPoint = deltaPoint.scalarDivide(1.5) - } - - this.canvas.relativePan(deltaPoint) - this.canvas._onMouseMove(event.e) - needSetCoords = true - }, - 16, // 1000 / 60 - { - immediate: false, - }, - ) - - // const { isSwiping } = useFabricSwipe({ - // onSwipeStart: () => { - // if (!this.edgeMoveStatus) return - // isSwiping.value = true - // resume() - // }, - // onSwipe: (e) => { - // if (!this.edgeMoveStatus) return - // event = e - // }, - // onSwipeEnd: () => { - // pause() - // event = undefined - // if (needSetCoords) { - // this.setCoords() - // needSetCoords = false - // } - // }, - // }) - - // this.eventbus.on('setEdgeMoveStatus', (value) => { - // this.edgeMoveStatus = value - // }) - } - - /** - * 判断点T相对于矩形的位置和距离 - * @param {Point} T - 待判断的点 - * @param {Point} A - 矩形左上角的点 - * @param {Point} B - 矩形右下角的点 - * @returns {Array} 第一个元素是pos,第二个元素是distance - */ - private judgePosition(T: Point, A: Point, B: Point): [number, number] { - let pos = 0 - let distance = 0 - if (T.x < A.x) (pos |= 1), (distance += A.x - T.x) - else if (T.x > B.x) (pos |= 2), (distance += T.x - B.x) - if (T.y < A.y) (pos |= 4), (distance += A.y - T.y) - else if (T.y > B.y) (pos |= 8), (distance += T.y - B.y) - return [pos, distance] - } -} diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue index 2a9c574..032b015 100644 --- a/src/components/FileUpload/index.vue +++ b/src/components/FileUpload/index.vue @@ -24,9 +24,7 @@ import { ElMessage, genFileId, UploadInstance, UploadProps, UploadRawFile } from import { uploadFile } from '@/api/file' import { useTemplatesStore } from '@/store' import { loadSVGFromString } from '@/extension/parser/loadSVGFromString' -import { ElementNames } from '@/types/elements' import { WorkSpaceDrawData, propertiesToInclude } from '@/configs/canvas' -import { Image, Object as FabricObject } from 'fabric' import { Template } from "@/types/canvas" import { nanoid } from 'nanoid' import useCanvasScale from '@/hooks/useCanvasScale' @@ -70,7 +68,7 @@ const generateSVGTemplate = async (dataText: string) => { const content = await loadSVGFromString(dataText) const options = content.options const svgData: any[] = [] - content.objects.slice(0, 1000).forEach(ele => svgData.push((ele as FabricObject).toObject(propertiesToInclude))) + content.objects.slice(0, 1000).forEach(ele => svgData.push(ele.toObject(propertiesToInclude))) WorkSpaceDrawData.width = options.width WorkSpaceDrawData.height = options.height const emptyTemplate: Template = { diff --git a/src/components/ReferencePopover.vue b/src/components/ReferencePopover.vue index fe2a7aa..e5775a5 100644 --- a/src/components/ReferencePopover.vue +++ b/src/components/ReferencePopover.vue @@ -31,7 +31,7 @@