| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- 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<RulerOptions>
- 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 })
- })
-
- 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<TPointerEvent>) {
- if (!e.viewportPoint) return
- if (this.tempReferenceLine && e.scenePoint) {
- const pos: Partial<ReferenceLine> = {};
- if (this.tempReferenceLine.axis === 'horizontal') {
- pos.top = e.scenePoint.y;
- }
- else {
- pos.left = e.scenePoint.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.viewportPoint)
- 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<TPointerEvent>) {
- const pointHover = this.getPointHover(e.viewportPoint)
- if (!pointHover) return
- if (this.activeOn === 'up') {
- this.canvas.selection = false
- this.activeOn = 'down'
- const point = pointHover === 'horizontal' ? e.viewportPoint.y : e.viewportPoint.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.addElement(this.tempReferenceLine)
- this.canvas.setActiveObject(this.tempReferenceLine)
- this.canvas._setupCurrentTransform(e.e, this.tempReferenceLine, true)
- this.tempReferenceLine.fire('down', this.getCommonEventInfo(e));
- }
- }
- private getCommonEventInfo(e: TPointerEventInfo<TPointerEvent>) {
- if (!this.tempReferenceLine || !e.scenePoint) return;
- return {
- e: e.e,
- transform: this.tempReferenceLine.get('transform'),
- pointer: {
- x: e.scenePoint.x,
- y: e.scenePoint.y,
- },
- target: this.tempReferenceLine,
- };
- }
- private mouseUp(e: TPointerEventInfo<TPointerEvent>) {
- 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()
- 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
- }
- }
|