@hprint/plugins 0.0.1-alpha.2 → 0.0.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.js +17 -17
  2. package/dist/index.mjs +1071 -1057
  3. package/dist/src/plugins/AlignGuidLinePlugin.d.ts +7 -2
  4. package/dist/src/plugins/AlignGuidLinePlugin.d.ts.map +1 -1
  5. package/dist/src/plugins/GroupAlignPlugin.d.ts.map +1 -1
  6. package/dist/src/plugins/LockPlugin.d.ts.map +1 -1
  7. package/dist/src/plugins/QrCodePlugin.d.ts +5 -0
  8. package/dist/src/plugins/QrCodePlugin.d.ts.map +1 -1
  9. package/package.json +3 -3
  10. package/src/assets/style/resizePlugin.css +27 -27
  11. package/src/objects/Arrow.js +47 -47
  12. package/src/objects/ThinTailArrow.js +50 -50
  13. package/src/plugins/AlignGuidLinePlugin.ts +1152 -1141
  14. package/src/plugins/BarCodePlugin.ts +2 -2
  15. package/src/plugins/ControlsPlugin.ts +251 -251
  16. package/src/plugins/ControlsRotatePlugin.ts +111 -111
  17. package/src/plugins/CopyPlugin.ts +255 -255
  18. package/src/plugins/DeleteHotKeyPlugin.ts +57 -57
  19. package/src/plugins/DrawLinePlugin.ts +162 -162
  20. package/src/plugins/DrawPolygonPlugin.ts +205 -205
  21. package/src/plugins/DringPlugin.ts +125 -125
  22. package/src/plugins/FlipPlugin.ts +59 -59
  23. package/src/plugins/FontPlugin.ts +165 -165
  24. package/src/plugins/FreeDrawPlugin.ts +49 -49
  25. package/src/plugins/GroupAlignPlugin.ts +365 -365
  26. package/src/plugins/GroupPlugin.ts +82 -82
  27. package/src/plugins/GroupTextEditorPlugin.ts +198 -198
  28. package/src/plugins/HistoryPlugin.ts +181 -181
  29. package/src/plugins/ImageStroke.ts +121 -121
  30. package/src/plugins/LayerPlugin.ts +108 -108
  31. package/src/plugins/LockPlugin.ts +242 -240
  32. package/src/plugins/MaskPlugin.ts +155 -155
  33. package/src/plugins/MaterialPlugin.ts +224 -224
  34. package/src/plugins/MiddleMousePlugin.ts +45 -45
  35. package/src/plugins/MoveHotKeyPlugin.ts +46 -46
  36. package/src/plugins/PathTextPlugin.ts +89 -89
  37. package/src/plugins/PolygonModifyPlugin.ts +224 -224
  38. package/src/plugins/PrintPlugin.ts +81 -81
  39. package/src/plugins/PsdPlugin.ts +52 -52
  40. package/src/plugins/QrCodePlugin.ts +322 -329
  41. package/src/plugins/ResizePlugin.ts +278 -278
  42. package/src/plugins/RulerPlugin.ts +78 -78
  43. package/src/plugins/SimpleClipImagePlugin.ts +244 -244
  44. package/src/plugins/UnitPlugin.ts +326 -326
  45. package/src/plugins/WaterMarkPlugin.ts +257 -257
  46. package/src/types/eventType.ts +11 -11
  47. package/src/utils/psd.js +432 -432
  48. package/src/utils/ruler/guideline.ts +145 -145
  49. package/src/utils/ruler/index.ts +91 -91
  50. package/src/utils/ruler/ruler.ts +924 -924
  51. package/src/utils/ruler/utils.ts +162 -162
  52. package/tsconfig.json +10 -10
  53. package/vite.config.ts +29 -29
@@ -1,924 +1,924 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { fabric, IEditor, Canvas, Point, IEvent } from '@hprint/core';
3
- import {
4
- getGap,
5
- mergeLines,
6
- darwRect,
7
- darwText,
8
- darwLine,
9
- drawMask,
10
- } from './utils';
11
- import { throttle } from 'lodash-es';
12
- import { setupGuideLine } from './guideline';
13
- import { LengthConvert } from '@hprint/shared';
14
- import { formatFixedString } from '../units';
15
-
16
- /**
17
- * 配置
18
- */
19
- export interface RulerOptions {
20
- /**
21
- * Canvas
22
- */
23
- canvas: Canvas;
24
-
25
- /**
26
- * Editor
27
- */
28
- editor: IEditor;
29
-
30
- /**
31
- * 单位(仅影响标尺文字展示)
32
- * @default 'px'
33
- */
34
- unit?: 'px' | 'mm';
35
-
36
- /**
37
- * 标尺宽高
38
- * @default 20
39
- */
40
- ruleSize?: number;
41
-
42
- /**
43
- * 字体大小
44
- * @default 10
45
- */
46
- fontSize?: number;
47
-
48
- /**
49
- * 是否开启标尺
50
- * @default false
51
- */
52
- enabled?: boolean;
53
-
54
- /**
55
- * 背景颜色
56
- */
57
- backgroundColor?: string;
58
-
59
- /**
60
- * 文字颜色
61
- */
62
- textColor?: string;
63
-
64
- /**
65
- * 边框颜色
66
- */
67
- borderColor?: string;
68
-
69
- /**
70
- * 高亮颜色
71
- */
72
- highlightColor?: string;
73
- }
74
-
75
- export type Rect = { left: number; top: number; width: number; height: number };
76
-
77
- export type HighlightRect = {
78
- skip?: 'x' | 'y';
79
- originXStart?: number;
80
- originXEnd?: number;
81
- originYStart?: number;
82
- originYEnd?: number;
83
- } & Rect;
84
-
85
- class CanvasRuler {
86
- protected ctx: CanvasRenderingContext2D;
87
-
88
- /**
89
- * 配置
90
- */
91
- public options: Required<RulerOptions>;
92
-
93
- /**
94
- * 当前单位
95
- */
96
- private unit: 'px' | 'mm' = 'px';
97
-
98
- /**
99
- * 标尺起始点
100
- */
101
- public startCalibration: undefined | Point;
102
-
103
- private activeOn: 'down' | 'up' = 'up';
104
-
105
- /**
106
- * 选取对象矩形坐标
107
- */
108
- private objectRect:
109
- | undefined
110
- | {
111
- x: HighlightRect[];
112
- y: HighlightRect[];
113
- };
114
-
115
- /**
116
- * 事件句柄缓存
117
- */
118
- private eventHandler: Record<string, (...args: any) => void> = {
119
- // calcCalibration: this.calcCalibration.bind(this),
120
- calcObjectRect: throttle(this.calcObjectRect.bind(this), 15),
121
- markRectDirtyImmediate: (e?: IEvent<Event>) => {
122
- this.currentMovingTarget = (e as any)?.target;
123
- this.needsRectUpdate = true;
124
- this.options.canvas.requestRenderAll();
125
- },
126
- markRectDirtyThrottled: throttle((e?: IEvent<Event>) => {
127
- this.currentMovingTarget = (e as any)?.target;
128
- this.needsRectUpdate = true;
129
- this.options.canvas.requestRenderAll();
130
- }, 30),
131
- clearStatus: this.clearStatus.bind(this),
132
- canvasMouseDown: this.canvasMouseDown.bind(this),
133
- canvasMouseMove: throttle(this.canvasMouseMove.bind(this), 15),
134
- canvasMouseUp: this.canvasMouseUp.bind(this),
135
- render: (e: any) => {
136
- // 避免多次渲染
137
- if (!e.ctx) return;
138
- this.render();
139
- },
140
- };
141
-
142
- private lastAttr: {
143
- status: 'out' | 'horizontal' | 'vertical';
144
- cursor: string | undefined;
145
- selection: boolean | undefined;
146
- } = {
147
- status: 'out',
148
- cursor: undefined,
149
- selection: undefined,
150
- };
151
-
152
- private tempGuidelLine: fabric.GuideLine | undefined;
153
- private needsRectUpdate: boolean = true;
154
- private currentMovingTarget: fabric.Object | undefined;
155
-
156
- constructor(_options: RulerOptions) {
157
- // 合并默认配置
158
- this.options = Object.assign(
159
- {
160
- ruleSize: 20,
161
- fontSize: 10,
162
- enabled: false,
163
- backgroundColor: '#fff',
164
- borderColor: '#ddd',
165
- highlightColor: '#007fff',
166
- textColor: '#888',
167
- unit: 'px',
168
- },
169
- _options
170
- );
171
-
172
- this.ctx = this.options.canvas.getContext();
173
- this.unit = this.options.unit || 'px';
174
-
175
- fabric.util.object.extend(this.options.canvas, {
176
- ruler: this,
177
- });
178
-
179
- setupGuideLine();
180
-
181
- if (this.options.enabled) {
182
- this.enable();
183
- }
184
- }
185
-
186
- // 设置单位
187
- public setUnit(unit: 'px' | 'mm') {
188
- if (this.unit === unit) return;
189
- this.unit = unit;
190
- this.render();
191
- }
192
-
193
- // 单位转换:像素 -> 显示单位数值
194
- private formatValueByUnit(pxValue: number) {
195
- if (this.unit === 'px') return pxValue;
196
- return LengthConvert.pxToMm(pxValue);
197
- }
198
-
199
- private formatLabel(pxValue: number) {
200
- const val = this.formatValueByUnit(pxValue);
201
- // 为保持清晰,毫米保留 1 位小数
202
- return this.unit === 'px'
203
- ? String(Math.round(val))
204
- : String(Math.round(val * 10) / 10);
205
- }
206
-
207
- // 销毁
208
- public destroy() {
209
- this.disable();
210
- }
211
-
212
- /**
213
- * 移除全部辅助线
214
- */
215
- public clearGuideline() {
216
- this.options.canvas.remove(
217
- ...this.options.canvas.getObjects(fabric.GuideLine.prototype.type)
218
- );
219
- }
220
-
221
- /**
222
- * 显示全部辅助线
223
- */
224
- public showGuideline() {
225
- this?.options?.canvas
226
- ?.getObjects(fabric.GuideLine.prototype.type)
227
- .forEach((guideLine) => {
228
- guideLine.set('visible', true);
229
- });
230
- this?.options?.canvas?.renderAll();
231
- }
232
-
233
- /**
234
- * 隐藏全部辅助线
235
- */
236
- public hideGuideline() {
237
- this.options.canvas
238
- .getObjects(fabric.GuideLine.prototype.type)
239
- .forEach((guideLine) => {
240
- guideLine.set('visible', false);
241
- });
242
- this.options.canvas.renderAll();
243
- }
244
-
245
- /**
246
- * 启用
247
- */
248
- public enable() {
249
- this.options.enabled = true;
250
-
251
- // 绑定事件
252
- this.options.canvas.on(
253
- 'after:render',
254
- this.eventHandler.calcObjectRect
255
- );
256
- this.options.canvas.on('after:render', this.eventHandler.render);
257
- this.options.canvas.on('mouse:down', this.eventHandler.canvasMouseDown);
258
- this.options.canvas.on('mouse:move', this.eventHandler.canvasMouseMove);
259
- this.options.canvas.on('mouse:up', this.eventHandler.canvasMouseUp);
260
- this.options.canvas.on(
261
- 'selection:cleared',
262
- this.eventHandler.clearStatus
263
- );
264
- this.options.canvas.on('object:added', this.eventHandler.markRectDirtyImmediate);
265
- this.options.canvas.on('selection:created', this.eventHandler.markRectDirtyImmediate);
266
- this.options.canvas.on('selection:updated', this.eventHandler.markRectDirtyImmediate);
267
- this.options.canvas.on('object:moving', this.eventHandler.markRectDirtyThrottled);
268
- this.options.canvas.on('object:modified', this.eventHandler.markRectDirtyImmediate);
269
- this.options.canvas.on('object:scaling', this.eventHandler.markRectDirtyThrottled);
270
- this.options.canvas.on('object:rotating', this.eventHandler.markRectDirtyThrottled);
271
-
272
- // 显示辅助线
273
- this.showGuideline();
274
-
275
- // 绘制一次
276
- this.render();
277
- }
278
-
279
- /**
280
- * 禁用
281
- */
282
- public disable() {
283
- // 解除事件
284
- this.options.canvas.off(
285
- 'after:render',
286
- this.eventHandler.calcObjectRect
287
- );
288
- this.options.canvas.off('after:render', this.eventHandler.render);
289
- this.options.canvas.off(
290
- 'mouse:down',
291
- this.eventHandler.canvasMouseDown
292
- );
293
- this.options.canvas.off(
294
- 'mouse:move',
295
- this.eventHandler.canvasMouseMove
296
- );
297
- this.options.canvas.off('mouse:up', this.eventHandler.canvasMouseUp);
298
- this.options.canvas.off(
299
- 'selection:cleared',
300
- this.eventHandler.clearStatus
301
- );
302
- this.options.canvas.off('object:added', this.eventHandler.markRectDirtyImmediate);
303
- this.options.canvas.off('selection:created', this.eventHandler.markRectDirtyImmediate);
304
- this.options.canvas.off('selection:updated', this.eventHandler.markRectDirtyImmediate);
305
- this.options.canvas.off('object:moving', this.eventHandler.markRectDirtyThrottled);
306
- this.options.canvas.off('object:modified', this.eventHandler.markRectDirtyImmediate);
307
- this.options.canvas.off('object:scaling', this.eventHandler.markRectDirtyThrottled);
308
- this.options.canvas.off('object:rotating', this.eventHandler.markRectDirtyThrottled);
309
-
310
- // 隐藏辅助线
311
- this.hideGuideline();
312
-
313
- this.options.enabled = false;
314
- }
315
-
316
- /**
317
- * 绘制
318
- */
319
- public render() {
320
- // if (!this.options.enabled) return;
321
- const vpt = this.options.canvas.viewportTransform;
322
- if (!vpt) return;
323
- if (this.needsRectUpdate) {
324
- if (this.currentMovingTarget) {
325
- this.calcObjectRectFromTarget(this.currentMovingTarget);
326
- } else {
327
- this.calcObjectRect();
328
- }
329
- this.currentMovingTarget = undefined;
330
- this.needsRectUpdate = false;
331
- }
332
- // 绘制尺子
333
- this.draw({
334
- isHorizontal: true,
335
- rulerLength: this.getSize().width,
336
- // startCalibration: -(vpt[4] / vpt[0]),
337
- startCalibration: this.startCalibration?.x
338
- ? this.startCalibration.x
339
- : -(vpt[4] / vpt[0]),
340
- });
341
- this.draw({
342
- isHorizontal: false,
343
- rulerLength: this.getSize().height,
344
- // startCalibration: -(vpt[5] / vpt[3]),
345
- startCalibration: this.startCalibration?.y
346
- ? this.startCalibration.y
347
- : -(vpt[5] / vpt[3]),
348
- });
349
- // 绘制左上角的遮罩
350
- drawMask(this.ctx, {
351
- isHorizontal: true,
352
- left: -10,
353
- top: -10,
354
- width: this.options.ruleSize * 2 + 10,
355
- height: this.options.ruleSize + 10,
356
- backgroundColor: this.options.backgroundColor,
357
- });
358
- drawMask(this.ctx, {
359
- isHorizontal: false,
360
- left: -10,
361
- top: -10,
362
- width: this.options.ruleSize + 10,
363
- height: this.options.ruleSize * 2 + 10,
364
- backgroundColor: this.options.backgroundColor,
365
- });
366
- }
367
-
368
- /**
369
- * 获取画板尺寸
370
- */
371
- private getSize() {
372
- return {
373
- width: this.options.canvas.width ?? 0,
374
- height: this.options.canvas.height ?? 0,
375
- };
376
- }
377
-
378
- private getZoom() {
379
- return this.options.canvas.getZoom();
380
- }
381
-
382
- private draw(opt: {
383
- isHorizontal: boolean;
384
- rulerLength: number;
385
- startCalibration: number;
386
- }) {
387
- const { isHorizontal, rulerLength, startCalibration } = opt;
388
- const zoom = this.getZoom();
389
-
390
- let gap = getGap(zoom);
391
- let mmStep = 1;
392
- if (this.unit === 'mm') {
393
- const mmPx = LengthConvert.mmToPx(1, undefined, { direct: true });
394
- const screenMm = mmPx * zoom;
395
- if (screenMm < 4) mmStep = 10;
396
- else if (screenMm < 8) mmStep = 5;
397
- gap = mmPx * mmStep;
398
- }
399
- const unitLength = rulerLength / zoom;
400
- const startValue =
401
- Math[startCalibration > 0 ? 'floor' : 'ceil'](
402
- startCalibration / gap
403
- ) * gap;
404
- const startOffset = startValue - startCalibration;
405
-
406
- // 标尺背景
407
- const canvasSize = this.getSize();
408
- darwRect(this.ctx, {
409
- left: 0,
410
- top: 0,
411
- width: isHorizontal ? canvasSize.width : this.options.ruleSize,
412
- height: isHorizontal ? this.options.ruleSize : canvasSize.height,
413
- fill: this.options.backgroundColor,
414
- stroke: this.options.borderColor,
415
- });
416
-
417
- // 颜色
418
- const textColor = new fabric.Color(this.options.textColor);
419
- // 标尺文字显示
420
- for (let i = 0; i + startOffset <= Math.ceil(unitLength); i += gap) {
421
- const position = (startOffset + i) * zoom;
422
- const textValue = this.formatLabel(startValue + i);
423
- if (this.unit === 'mm') {
424
- const mmVal = LengthConvert.pxToMm(startValue + i);
425
- const mmRounded = Math.round(mmVal);
426
- if (mmRounded % 5 !== 0) {
427
- continue;
428
- }
429
- }
430
- const textLength = (10 * textValue.length) / 4;
431
- const textX = isHorizontal
432
- ? position - textLength - 1
433
- : this.options.ruleSize / 2 - this.options.fontSize / 2 - 4;
434
- const textY = isHorizontal
435
- ? this.options.ruleSize / 2 - this.options.fontSize / 2 - 4
436
- : position + textLength;
437
- darwText(this.ctx, {
438
- text: textValue,
439
- left: textX,
440
- top: textY,
441
- fill: textColor.toRgb(),
442
- angle: isHorizontal ? 0 : -90,
443
- });
444
- }
445
-
446
- // 标尺刻度线显示
447
- for (let j = 0; j + startOffset <= Math.ceil(unitLength); j += gap) {
448
- const position = Math.round((startOffset + j) * zoom);
449
- let lineSize = 8;
450
- if (this.unit === 'mm') {
451
- const mmVal = LengthConvert.pxToMm(startValue + j);
452
- const mmRounded = Math.round(mmVal);
453
- if (mmRounded % 10 === 0) lineSize = 12;
454
- else if (mmRounded % 5 === 0) lineSize = 10;
455
- else lineSize = 6;
456
- }
457
- const left = isHorizontal ? position : this.options.ruleSize - lineSize;
458
- const top = isHorizontal ? this.options.ruleSize - lineSize : position;
459
- const width = isHorizontal ? 0 : lineSize;
460
- const height = isHorizontal ? lineSize : 0;
461
- darwLine(this.ctx, {
462
- left,
463
- top,
464
- width,
465
- height,
466
- stroke: textColor.toRgb(),
467
- });
468
- }
469
-
470
- // 标尺蓝色遮罩
471
- if (this.objectRect) {
472
- const axis = isHorizontal ? 'x' : 'y';
473
- this.objectRect[axis].forEach((rect) => {
474
- // 跳过指定矩形
475
- if (rect.skip === axis) {
476
- return;
477
- }
478
-
479
- // 获取数字的值
480
- const getOriginValue = (type: 'start' | 'end') => {
481
- if (this.unit === 'px') {
482
- const value = this.options.editor.getSizeByUnit(
483
- (type === 'start'
484
- ? isHorizontal
485
- ? rect.left
486
- : rect.top
487
- : isHorizontal
488
- ? rect.left + rect.width
489
- : rect.top + rect.height) /
490
- zoom +
491
- startCalibration
492
- );
493
- const num = Number(value);
494
- if (Number.isNaN(num)) return undefined;
495
- return Math.round(num * 100) / 100;
496
- }
497
- if (isHorizontal) {
498
- return type === 'start' ? rect.originXStart : rect.originXEnd;
499
- } else {
500
- return type === 'start' ? rect.originYStart : rect.originYEnd;
501
- }
502
- };
503
-
504
- const leftTextVal = getOriginValue('start');
505
- const rightTextVal = getOriginValue('end');
506
- const precision = this.options.editor.getPrecision?.() ?? 2;
507
- const leftTextStr =
508
- leftTextVal !== undefined ? formatFixedString(leftTextVal, precision) : undefined;
509
- const rightTextStr =
510
- rightTextVal !== undefined ? formatFixedString(rightTextVal, precision) : undefined;
511
-
512
- const isSameText =
513
- leftTextVal !== undefined &&
514
- rightTextVal !== undefined &&
515
- leftTextVal === rightTextVal;
516
-
517
- // 背景遮罩
518
- const maskOpt = {
519
- isHorizontal,
520
- width: isHorizontal ? 160 : this.options.ruleSize - 8,
521
- height: isHorizontal ? this.options.ruleSize - 8 : 160,
522
- backgroundColor: this.options.backgroundColor,
523
- };
524
- drawMask(this.ctx, {
525
- ...maskOpt,
526
- left: isHorizontal ? rect.left - 80 : 0,
527
- top: isHorizontal ? 0 : rect.top - 80,
528
- });
529
- if (!isSameText) {
530
- drawMask(this.ctx, {
531
- ...maskOpt,
532
- left: isHorizontal ? rect.width + rect.left - 80 : 0,
533
- top: isHorizontal ? 0 : rect.height + rect.top - 80,
534
- });
535
- }
536
-
537
- // 颜色
538
- const highlightColor = new fabric.Color(
539
- this.options.highlightColor
540
- );
541
-
542
- // 高亮遮罩
543
- highlightColor.setAlpha(0.5);
544
- darwRect(this.ctx, {
545
- left: isHorizontal ? rect.left : this.options.ruleSize - 8,
546
- top: isHorizontal ? this.options.ruleSize - 8 : rect.top,
547
- width: isHorizontal ? rect.width : 8,
548
- height: isHorizontal ? 8 : rect.height,
549
- fill: highlightColor.toRgba(),
550
- });
551
-
552
- // 两边的数字
553
- const pad =
554
- this.options.ruleSize / 2 - this.options.fontSize / 2 - 4;
555
-
556
- const textOpt = {
557
- fill: highlightColor.toRgba(),
558
- angle: isHorizontal ? 0 : -90,
559
- };
560
-
561
- if (leftTextStr !== undefined) {
562
- darwText(this.ctx, {
563
- ...textOpt,
564
- text: leftTextStr,
565
- left: isHorizontal ? rect.left - 2 : pad,
566
- top: isHorizontal ? pad : rect.top - 2,
567
- align: isSameText
568
- ? 'center'
569
- : isHorizontal
570
- ? 'right'
571
- : 'left',
572
- });
573
- }
574
-
575
- if (!isSameText && rightTextStr !== undefined) {
576
- darwText(this.ctx, {
577
- ...textOpt,
578
- text: rightTextStr,
579
- left: isHorizontal ? rect.left + rect.width + 2 : pad,
580
- top: isHorizontal ? pad : rect.top + rect.height + 2,
581
- align: isHorizontal ? 'left' : 'right',
582
- });
583
- }
584
-
585
- // 两边的线
586
- const lineSize = isSameText ? 8 : 14;
587
-
588
- highlightColor.setAlpha(1);
589
-
590
- const lineOpt = {
591
- width: isHorizontal ? 0 : lineSize,
592
- height: isHorizontal ? lineSize : 0,
593
- stroke: highlightColor.toRgba(),
594
- };
595
-
596
- darwLine(this.ctx, {
597
- ...lineOpt,
598
- left: isHorizontal
599
- ? rect.left
600
- : this.options.ruleSize - lineSize,
601
- top: isHorizontal
602
- ? this.options.ruleSize - lineSize
603
- : rect.top,
604
- });
605
-
606
- if (!isSameText) {
607
- darwLine(this.ctx, {
608
- ...lineOpt,
609
- left: isHorizontal
610
- ? rect.left + rect.width
611
- : this.options.ruleSize - lineSize,
612
- top: isHorizontal
613
- ? this.options.ruleSize - lineSize
614
- : rect.top + rect.height,
615
- });
616
- }
617
- });
618
- }
619
- // draw end
620
- }
621
-
622
- /**
623
- * 计算起始点
624
- */
625
- // private calcCalibration() {
626
- // if (this.startCalibration) return;
627
- // // console.log('calcCalibration');
628
- // const workspace = this.options.canvas.getObjects().find((item: any) => {
629
- // return item.id === 'workspace';
630
- // });
631
- // if (!workspace) return;
632
- // const rect = workspace.getBoundingRect(false);
633
- // this.startCalibration = new fabric.Point(-rect.left, -rect.top).divide(this.getZoom());
634
- // }
635
-
636
- private calcObjectRect() {
637
- const activeObjects = this.options.canvas.getActiveObjects();
638
- if (activeObjects.length === 0) return;
639
- const allRect = activeObjects.reduce((rects, obj) => {
640
- const rect: HighlightRect = obj.getBoundingRect(false, true);
641
- // 如果是分组单独计算坐标
642
- if (obj.group) {
643
- const baseGroup: any = obj.group;
644
- const group = {
645
- ...baseGroup,
646
- scaleX: baseGroup?.scaleX ?? 1,
647
- scaleY: baseGroup?.scaleY ?? 1,
648
- } as any;
649
- // 计算矩形坐标
650
- rect.width *= group.scaleX;
651
- rect.height *= group.scaleY;
652
- const groupCenterX = group.width / 2 + group.left;
653
- const objectOffsetFromCenterX =
654
- (group.width / 2 + (obj.left ?? 0)) * (1 - group.scaleX);
655
- rect.left +=
656
- (groupCenterX - objectOffsetFromCenterX) * this.getZoom();
657
- const groupCenterY = group.height / 2 + group.top;
658
- const objectOffsetFromCenterY =
659
- (group.height / 2 + (obj.top ?? 0)) * (1 - group.scaleY);
660
- rect.top +=
661
- (groupCenterY - objectOffsetFromCenterY) * this.getZoom();
662
- }
663
- if (obj instanceof fabric.GuideLine) {
664
- rect.skip = obj.isHorizontal() ? 'x' : 'y';
665
- }
666
- if (this.unit !== 'px') {
667
- const origin = (obj as any)._originSize?.[this.unit];
668
- if (origin) {
669
- if (obj instanceof fabric.Polygon && Array.isArray(origin.points)) {
670
- const xs = origin.points.map((p: any) => p.x);
671
- const ys = origin.points.map((p: any) => p.y);
672
- const minX = Math.min(...xs);
673
- const maxX = Math.max(...xs);
674
- const minY = Math.min(...ys);
675
- const maxY = Math.max(...ys);
676
- rect.originXStart = minX;
677
- rect.originXEnd = maxX;
678
- rect.originYStart = minY;
679
- rect.originYEnd = maxY;
680
- } else {
681
- const left = origin.left;
682
- const top = origin.top;
683
- let width = origin.width;
684
- let height = origin.height;
685
- if (width === undefined && origin.rx !== undefined) {
686
- width = origin.rx * 2;
687
- }
688
- if (height === undefined && origin.ry !== undefined) {
689
- height = origin.ry * 2;
690
- }
691
- if (left !== undefined && width !== undefined) {
692
- rect.originXStart = left;
693
- rect.originXEnd = left + width;
694
- }
695
- if (top !== undefined && height !== undefined) {
696
- rect.originYStart = top;
697
- rect.originYEnd = top + height;
698
- }
699
- }
700
- }
701
- }
702
- rects.push(rect);
703
- return rects;
704
- }, [] as HighlightRect[]);
705
- if (allRect.length === 0) return;
706
- this.objectRect = {
707
- x: mergeLines(allRect, true),
708
- y: mergeLines(allRect, false),
709
- };
710
- }
711
-
712
- private calcObjectRectFromTarget(target: fabric.Object) {
713
- const getRectFromObj = (obj: fabric.Object): HighlightRect => {
714
- const rect: HighlightRect = obj.getBoundingRect(false, true);
715
- if ((obj as any).group) {
716
- const baseGroup: any = (obj as any).group;
717
- const group = {
718
- ...baseGroup,
719
- scaleX: baseGroup?.scaleX ?? 1,
720
- scaleY: baseGroup?.scaleY ?? 1,
721
- } as any;
722
- rect.width *= group.scaleX;
723
- rect.height *= group.scaleY;
724
- const groupCenterX = group.width / 2 + group.left;
725
- const objectOffsetFromCenterX =
726
- (group.width / 2 + (obj.left ?? 0)) * (1 - group.scaleX);
727
- rect.left +=
728
- (groupCenterX - objectOffsetFromCenterX) * this.getZoom();
729
- const groupCenterY = group.height / 2 + group.top;
730
- const objectOffsetFromCenterY =
731
- (group.height / 2 + (obj.top ?? 0)) * (1 - group.scaleY);
732
- rect.top +=
733
- (groupCenterY - objectOffsetFromCenterY) * this.getZoom();
734
- }
735
- if ((obj as any) instanceof fabric.GuideLine) {
736
- (rect as any).skip = (obj as any).isHorizontal() ? 'x' : 'y';
737
- }
738
- if (this.unit !== 'px') {
739
- const origin = (obj as any)._originSize?.[this.unit];
740
- if (origin) {
741
- if ((obj as any) instanceof fabric.Polygon && Array.isArray(origin.points)) {
742
- const xs = origin.points.map((p: any) => p.x);
743
- const ys = origin.points.map((p: any) => p.y);
744
- const minX = Math.min(...xs);
745
- const maxX = Math.max(...xs);
746
- const minY = Math.min(...ys);
747
- const maxY = Math.max(...ys);
748
- rect.originXStart = minX;
749
- rect.originXEnd = maxX;
750
- rect.originYStart = minY;
751
- rect.originYEnd = maxY;
752
- } else {
753
- const left = origin.left;
754
- const top = origin.top;
755
- let width = origin.width;
756
- let height = origin.height;
757
- if (width === undefined && origin.rx !== undefined) {
758
- width = origin.rx * 2;
759
- }
760
- if (height === undefined && origin.ry !== undefined) {
761
- height = origin.ry * 2;
762
- }
763
- if (left !== undefined && width !== undefined) {
764
- rect.originXStart = left;
765
- rect.originXEnd = left + width;
766
- }
767
- if (top !== undefined && height !== undefined) {
768
- rect.originYStart = top;
769
- rect.originYEnd = top + height;
770
- }
771
- }
772
- }
773
- }
774
- return rect;
775
- };
776
-
777
- const rect = getRectFromObj(target);
778
- this.objectRect = {
779
- x: mergeLines([rect], true),
780
- y: mergeLines([rect], false),
781
- };
782
- }
783
-
784
- /**
785
- * 清除起始点和矩形坐标
786
- */
787
- private clearStatus() {
788
- // this.startCalibration = undefined;
789
- this.objectRect = undefined;
790
- }
791
-
792
- /**
793
- 判断鼠标是否在标尺上
794
- * @param point
795
- * @returns "vertical" | "horizontal" | false
796
- */
797
- public isPointOnRuler(point: Point) {
798
- if (
799
- new fabric.Rect({
800
- left: 0,
801
- top: 0,
802
- width: this.options.ruleSize,
803
- height: this.options.canvas.height,
804
- }).containsPoint(point)
805
- ) {
806
- return 'vertical';
807
- } else if (
808
- new fabric.Rect({
809
- left: 0,
810
- top: 0,
811
- width: this.options.canvas.width,
812
- height: this.options.ruleSize,
813
- }).containsPoint(point)
814
- ) {
815
- return 'horizontal';
816
- }
817
- return false;
818
- }
819
-
820
- private canvasMouseDown(e: IEvent<MouseEvent>) {
821
- if (!e.pointer || !e.absolutePointer) return;
822
- const hoveredRuler = this.isPointOnRuler(e.pointer);
823
- if (hoveredRuler && this.activeOn === 'up') {
824
- // 备份属性
825
- this.lastAttr.selection = this.options.canvas.selection;
826
- this.options.canvas.selection = false;
827
- this.activeOn = 'down';
828
-
829
- this.tempGuidelLine = new fabric.GuideLine(
830
- hoveredRuler === 'horizontal'
831
- ? e.absolutePointer.y
832
- : e.absolutePointer.x,
833
- {
834
- axis: hoveredRuler,
835
- visible: false,
836
- }
837
- );
838
-
839
- this.options.canvas.add(this.tempGuidelLine);
840
- this.options.canvas.setActiveObject(this.tempGuidelLine);
841
-
842
- this.options.canvas._setupCurrentTransform(
843
- e.e,
844
- this.tempGuidelLine,
845
- true
846
- );
847
-
848
- this.tempGuidelLine.fire('down', this.getCommonEventInfo(e));
849
- }
850
- }
851
-
852
- private getCommonEventInfo = (e: IEvent<MouseEvent>) => {
853
- if (!this.tempGuidelLine || !e.absolutePointer) return;
854
- return {
855
- e: e.e,
856
- transform: this.tempGuidelLine.get('transform'),
857
- pointer: {
858
- x: e.absolutePointer.x,
859
- y: e.absolutePointer.y,
860
- },
861
- target: this.tempGuidelLine,
862
- };
863
- };
864
-
865
- private canvasMouseMove(e: IEvent<MouseEvent>) {
866
- if (!e.pointer) return;
867
-
868
- if (this.tempGuidelLine && e.absolutePointer) {
869
- const pos: Partial<fabric.IGuideLineOptions> = {};
870
- if (this.tempGuidelLine.axis === 'horizontal') {
871
- pos.top = e.absolutePointer.y;
872
- } else {
873
- pos.left = e.absolutePointer.x;
874
- }
875
- this.tempGuidelLine.set({ ...pos, visible: true });
876
-
877
- this.options.canvas.requestRenderAll();
878
-
879
- const event = this.getCommonEventInfo(e);
880
- this.options.canvas.fire('object:moving', event);
881
- this.tempGuidelLine.fire('moving', event);
882
- }
883
-
884
- const hoveredRuler = this.isPointOnRuler(e.pointer);
885
- if (!hoveredRuler) {
886
- // 鼠标从里面出去
887
- if (this.lastAttr.status !== 'out') {
888
- // 更改鼠标指针
889
- this.options.canvas.defaultCursor = this.lastAttr.cursor;
890
- this.lastAttr.status = 'out';
891
- }
892
- return;
893
- }
894
- // const activeObjects = this.options.canvas.getActiveObjects();
895
- // if (activeObjects.length === 1 && activeObjects[0] instanceof fabric.GuideLine) {
896
- // return;
897
- // }
898
- // 鼠标从外边进入 或 在另一侧标尺
899
- if (
900
- this.lastAttr.status === 'out' ||
901
- hoveredRuler !== this.lastAttr.status
902
- ) {
903
- // 更改鼠标指针
904
- this.lastAttr.cursor = this.options.canvas.defaultCursor;
905
- this.options.canvas.defaultCursor =
906
- hoveredRuler === 'horizontal' ? 'ns-resize' : 'ew-resize';
907
- this.lastAttr.status = hoveredRuler;
908
- }
909
- }
910
-
911
- private canvasMouseUp(e: IEvent<MouseEvent>) {
912
- if (this.activeOn !== 'down') return;
913
-
914
- // 还原属性
915
- this.options.canvas.selection = this.lastAttr.selection;
916
- this.activeOn = 'up';
917
-
918
- this.tempGuidelLine?.fire('up', this.getCommonEventInfo(e));
919
-
920
- this.tempGuidelLine = undefined;
921
- }
922
- }
923
-
924
- export default CanvasRuler;
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { fabric, IEditor, Canvas, Point, IEvent } from '@hprint/core';
3
+ import {
4
+ getGap,
5
+ mergeLines,
6
+ darwRect,
7
+ darwText,
8
+ darwLine,
9
+ drawMask,
10
+ } from './utils';
11
+ import { throttle } from 'lodash-es';
12
+ import { setupGuideLine } from './guideline';
13
+ import { LengthConvert } from '@hprint/shared';
14
+ import { formatFixedString } from '../units';
15
+
16
+ /**
17
+ * 配置
18
+ */
19
+ export interface RulerOptions {
20
+ /**
21
+ * Canvas
22
+ */
23
+ canvas: Canvas;
24
+
25
+ /**
26
+ * Editor
27
+ */
28
+ editor: IEditor;
29
+
30
+ /**
31
+ * 单位(仅影响标尺文字展示)
32
+ * @default 'px'
33
+ */
34
+ unit?: 'px' | 'mm';
35
+
36
+ /**
37
+ * 标尺宽高
38
+ * @default 20
39
+ */
40
+ ruleSize?: number;
41
+
42
+ /**
43
+ * 字体大小
44
+ * @default 10
45
+ */
46
+ fontSize?: number;
47
+
48
+ /**
49
+ * 是否开启标尺
50
+ * @default false
51
+ */
52
+ enabled?: boolean;
53
+
54
+ /**
55
+ * 背景颜色
56
+ */
57
+ backgroundColor?: string;
58
+
59
+ /**
60
+ * 文字颜色
61
+ */
62
+ textColor?: string;
63
+
64
+ /**
65
+ * 边框颜色
66
+ */
67
+ borderColor?: string;
68
+
69
+ /**
70
+ * 高亮颜色
71
+ */
72
+ highlightColor?: string;
73
+ }
74
+
75
+ export type Rect = { left: number; top: number; width: number; height: number };
76
+
77
+ export type HighlightRect = {
78
+ skip?: 'x' | 'y';
79
+ originXStart?: number;
80
+ originXEnd?: number;
81
+ originYStart?: number;
82
+ originYEnd?: number;
83
+ } & Rect;
84
+
85
+ class CanvasRuler {
86
+ protected ctx: CanvasRenderingContext2D;
87
+
88
+ /**
89
+ * 配置
90
+ */
91
+ public options: Required<RulerOptions>;
92
+
93
+ /**
94
+ * 当前单位
95
+ */
96
+ private unit: 'px' | 'mm' = 'px';
97
+
98
+ /**
99
+ * 标尺起始点
100
+ */
101
+ public startCalibration: undefined | Point;
102
+
103
+ private activeOn: 'down' | 'up' = 'up';
104
+
105
+ /**
106
+ * 选取对象矩形坐标
107
+ */
108
+ private objectRect:
109
+ | undefined
110
+ | {
111
+ x: HighlightRect[];
112
+ y: HighlightRect[];
113
+ };
114
+
115
+ /**
116
+ * 事件句柄缓存
117
+ */
118
+ private eventHandler: Record<string, (...args: any) => void> = {
119
+ // calcCalibration: this.calcCalibration.bind(this),
120
+ calcObjectRect: throttle(this.calcObjectRect.bind(this), 15),
121
+ markRectDirtyImmediate: (e?: IEvent<Event>) => {
122
+ this.currentMovingTarget = (e as any)?.target;
123
+ this.needsRectUpdate = true;
124
+ this.options.canvas.requestRenderAll();
125
+ },
126
+ markRectDirtyThrottled: throttle((e?: IEvent<Event>) => {
127
+ this.currentMovingTarget = (e as any)?.target;
128
+ this.needsRectUpdate = true;
129
+ this.options.canvas.requestRenderAll();
130
+ }, 30),
131
+ clearStatus: this.clearStatus.bind(this),
132
+ canvasMouseDown: this.canvasMouseDown.bind(this),
133
+ canvasMouseMove: throttle(this.canvasMouseMove.bind(this), 15),
134
+ canvasMouseUp: this.canvasMouseUp.bind(this),
135
+ render: (e: any) => {
136
+ // 避免多次渲染
137
+ if (!e.ctx) return;
138
+ this.render();
139
+ },
140
+ };
141
+
142
+ private lastAttr: {
143
+ status: 'out' | 'horizontal' | 'vertical';
144
+ cursor: string | undefined;
145
+ selection: boolean | undefined;
146
+ } = {
147
+ status: 'out',
148
+ cursor: undefined,
149
+ selection: undefined,
150
+ };
151
+
152
+ private tempGuidelLine: fabric.GuideLine | undefined;
153
+ private needsRectUpdate: boolean = true;
154
+ private currentMovingTarget: fabric.Object | undefined;
155
+
156
+ constructor(_options: RulerOptions) {
157
+ // 合并默认配置
158
+ this.options = Object.assign(
159
+ {
160
+ ruleSize: 20,
161
+ fontSize: 10,
162
+ enabled: false,
163
+ backgroundColor: '#fff',
164
+ borderColor: '#ddd',
165
+ highlightColor: '#007fff',
166
+ textColor: '#888',
167
+ unit: 'px',
168
+ },
169
+ _options
170
+ );
171
+
172
+ this.ctx = this.options.canvas.getContext();
173
+ this.unit = this.options.unit || 'px';
174
+
175
+ fabric.util.object.extend(this.options.canvas, {
176
+ ruler: this,
177
+ });
178
+
179
+ setupGuideLine();
180
+
181
+ if (this.options.enabled) {
182
+ this.enable();
183
+ }
184
+ }
185
+
186
+ // 设置单位
187
+ public setUnit(unit: 'px' | 'mm') {
188
+ if (this.unit === unit) return;
189
+ this.unit = unit;
190
+ this.render();
191
+ }
192
+
193
+ // 单位转换:像素 -> 显示单位数值
194
+ private formatValueByUnit(pxValue: number) {
195
+ if (this.unit === 'px') return pxValue;
196
+ return LengthConvert.pxToMm(pxValue);
197
+ }
198
+
199
+ private formatLabel(pxValue: number) {
200
+ const val = this.formatValueByUnit(pxValue);
201
+ // 为保持清晰,毫米保留 1 位小数
202
+ return this.unit === 'px'
203
+ ? String(Math.round(val))
204
+ : String(Math.round(val * 10) / 10);
205
+ }
206
+
207
+ // 销毁
208
+ public destroy() {
209
+ this.disable();
210
+ }
211
+
212
+ /**
213
+ * 移除全部辅助线
214
+ */
215
+ public clearGuideline() {
216
+ this.options.canvas.remove(
217
+ ...this.options.canvas.getObjects(fabric.GuideLine.prototype.type)
218
+ );
219
+ }
220
+
221
+ /**
222
+ * 显示全部辅助线
223
+ */
224
+ public showGuideline() {
225
+ this?.options?.canvas
226
+ ?.getObjects(fabric.GuideLine.prototype.type)
227
+ .forEach((guideLine) => {
228
+ guideLine.set('visible', true);
229
+ });
230
+ this?.options?.canvas?.renderAll();
231
+ }
232
+
233
+ /**
234
+ * 隐藏全部辅助线
235
+ */
236
+ public hideGuideline() {
237
+ this.options.canvas
238
+ .getObjects(fabric.GuideLine.prototype.type)
239
+ .forEach((guideLine) => {
240
+ guideLine.set('visible', false);
241
+ });
242
+ this.options.canvas.renderAll();
243
+ }
244
+
245
+ /**
246
+ * 启用
247
+ */
248
+ public enable() {
249
+ this.options.enabled = true;
250
+
251
+ // 绑定事件
252
+ this.options.canvas.on(
253
+ 'after:render',
254
+ this.eventHandler.calcObjectRect
255
+ );
256
+ this.options.canvas.on('after:render', this.eventHandler.render);
257
+ this.options.canvas.on('mouse:down', this.eventHandler.canvasMouseDown);
258
+ this.options.canvas.on('mouse:move', this.eventHandler.canvasMouseMove);
259
+ this.options.canvas.on('mouse:up', this.eventHandler.canvasMouseUp);
260
+ this.options.canvas.on(
261
+ 'selection:cleared',
262
+ this.eventHandler.clearStatus
263
+ );
264
+ this.options.canvas.on('object:added', this.eventHandler.markRectDirtyImmediate);
265
+ this.options.canvas.on('selection:created', this.eventHandler.markRectDirtyImmediate);
266
+ this.options.canvas.on('selection:updated', this.eventHandler.markRectDirtyImmediate);
267
+ this.options.canvas.on('object:moving', this.eventHandler.markRectDirtyThrottled);
268
+ this.options.canvas.on('object:modified', this.eventHandler.markRectDirtyImmediate);
269
+ this.options.canvas.on('object:scaling', this.eventHandler.markRectDirtyThrottled);
270
+ this.options.canvas.on('object:rotating', this.eventHandler.markRectDirtyThrottled);
271
+
272
+ // 显示辅助线
273
+ this.showGuideline();
274
+
275
+ // 绘制一次
276
+ this.render();
277
+ }
278
+
279
+ /**
280
+ * 禁用
281
+ */
282
+ public disable() {
283
+ // 解除事件
284
+ this.options.canvas.off(
285
+ 'after:render',
286
+ this.eventHandler.calcObjectRect
287
+ );
288
+ this.options.canvas.off('after:render', this.eventHandler.render);
289
+ this.options.canvas.off(
290
+ 'mouse:down',
291
+ this.eventHandler.canvasMouseDown
292
+ );
293
+ this.options.canvas.off(
294
+ 'mouse:move',
295
+ this.eventHandler.canvasMouseMove
296
+ );
297
+ this.options.canvas.off('mouse:up', this.eventHandler.canvasMouseUp);
298
+ this.options.canvas.off(
299
+ 'selection:cleared',
300
+ this.eventHandler.clearStatus
301
+ );
302
+ this.options.canvas.off('object:added', this.eventHandler.markRectDirtyImmediate);
303
+ this.options.canvas.off('selection:created', this.eventHandler.markRectDirtyImmediate);
304
+ this.options.canvas.off('selection:updated', this.eventHandler.markRectDirtyImmediate);
305
+ this.options.canvas.off('object:moving', this.eventHandler.markRectDirtyThrottled);
306
+ this.options.canvas.off('object:modified', this.eventHandler.markRectDirtyImmediate);
307
+ this.options.canvas.off('object:scaling', this.eventHandler.markRectDirtyThrottled);
308
+ this.options.canvas.off('object:rotating', this.eventHandler.markRectDirtyThrottled);
309
+
310
+ // 隐藏辅助线
311
+ this.hideGuideline();
312
+
313
+ this.options.enabled = false;
314
+ }
315
+
316
+ /**
317
+ * 绘制
318
+ */
319
+ public render() {
320
+ // if (!this.options.enabled) return;
321
+ const vpt = this.options.canvas.viewportTransform;
322
+ if (!vpt) return;
323
+ if (this.needsRectUpdate) {
324
+ if (this.currentMovingTarget) {
325
+ this.calcObjectRectFromTarget(this.currentMovingTarget);
326
+ } else {
327
+ this.calcObjectRect();
328
+ }
329
+ this.currentMovingTarget = undefined;
330
+ this.needsRectUpdate = false;
331
+ }
332
+ // 绘制尺子
333
+ this.draw({
334
+ isHorizontal: true,
335
+ rulerLength: this.getSize().width,
336
+ // startCalibration: -(vpt[4] / vpt[0]),
337
+ startCalibration: this.startCalibration?.x
338
+ ? this.startCalibration.x
339
+ : -(vpt[4] / vpt[0]),
340
+ });
341
+ this.draw({
342
+ isHorizontal: false,
343
+ rulerLength: this.getSize().height,
344
+ // startCalibration: -(vpt[5] / vpt[3]),
345
+ startCalibration: this.startCalibration?.y
346
+ ? this.startCalibration.y
347
+ : -(vpt[5] / vpt[3]),
348
+ });
349
+ // 绘制左上角的遮罩
350
+ drawMask(this.ctx, {
351
+ isHorizontal: true,
352
+ left: -10,
353
+ top: -10,
354
+ width: this.options.ruleSize * 2 + 10,
355
+ height: this.options.ruleSize + 10,
356
+ backgroundColor: this.options.backgroundColor,
357
+ });
358
+ drawMask(this.ctx, {
359
+ isHorizontal: false,
360
+ left: -10,
361
+ top: -10,
362
+ width: this.options.ruleSize + 10,
363
+ height: this.options.ruleSize * 2 + 10,
364
+ backgroundColor: this.options.backgroundColor,
365
+ });
366
+ }
367
+
368
+ /**
369
+ * 获取画板尺寸
370
+ */
371
+ private getSize() {
372
+ return {
373
+ width: this.options.canvas.width ?? 0,
374
+ height: this.options.canvas.height ?? 0,
375
+ };
376
+ }
377
+
378
+ private getZoom() {
379
+ return this.options.canvas.getZoom();
380
+ }
381
+
382
+ private draw(opt: {
383
+ isHorizontal: boolean;
384
+ rulerLength: number;
385
+ startCalibration: number;
386
+ }) {
387
+ const { isHorizontal, rulerLength, startCalibration } = opt;
388
+ const zoom = this.getZoom();
389
+
390
+ let gap = getGap(zoom);
391
+ let mmStep = 1;
392
+ if (this.unit === 'mm') {
393
+ const mmPx = LengthConvert.mmToPx(1, undefined, { direct: true });
394
+ const screenMm = mmPx * zoom;
395
+ if (screenMm < 4) mmStep = 10;
396
+ else if (screenMm < 8) mmStep = 5;
397
+ gap = mmPx * mmStep;
398
+ }
399
+ const unitLength = rulerLength / zoom;
400
+ const startValue =
401
+ Math[startCalibration > 0 ? 'floor' : 'ceil'](
402
+ startCalibration / gap
403
+ ) * gap;
404
+ const startOffset = startValue - startCalibration;
405
+
406
+ // 标尺背景
407
+ const canvasSize = this.getSize();
408
+ darwRect(this.ctx, {
409
+ left: 0,
410
+ top: 0,
411
+ width: isHorizontal ? canvasSize.width : this.options.ruleSize,
412
+ height: isHorizontal ? this.options.ruleSize : canvasSize.height,
413
+ fill: this.options.backgroundColor,
414
+ stroke: this.options.borderColor,
415
+ });
416
+
417
+ // 颜色
418
+ const textColor = new fabric.Color(this.options.textColor);
419
+ // 标尺文字显示
420
+ for (let i = 0; i + startOffset <= Math.ceil(unitLength); i += gap) {
421
+ const position = (startOffset + i) * zoom;
422
+ const textValue = this.formatLabel(startValue + i);
423
+ if (this.unit === 'mm') {
424
+ const mmVal = LengthConvert.pxToMm(startValue + i);
425
+ const mmRounded = Math.round(mmVal);
426
+ if (mmRounded % 5 !== 0) {
427
+ continue;
428
+ }
429
+ }
430
+ const textLength = (10 * textValue.length) / 4;
431
+ const textX = isHorizontal
432
+ ? position - textLength - 1
433
+ : this.options.ruleSize / 2 - this.options.fontSize / 2 - 4;
434
+ const textY = isHorizontal
435
+ ? this.options.ruleSize / 2 - this.options.fontSize / 2 - 4
436
+ : position + textLength;
437
+ darwText(this.ctx, {
438
+ text: textValue,
439
+ left: textX,
440
+ top: textY,
441
+ fill: textColor.toRgb(),
442
+ angle: isHorizontal ? 0 : -90,
443
+ });
444
+ }
445
+
446
+ // 标尺刻度线显示
447
+ for (let j = 0; j + startOffset <= Math.ceil(unitLength); j += gap) {
448
+ const position = Math.round((startOffset + j) * zoom);
449
+ let lineSize = 8;
450
+ if (this.unit === 'mm') {
451
+ const mmVal = LengthConvert.pxToMm(startValue + j);
452
+ const mmRounded = Math.round(mmVal);
453
+ if (mmRounded % 10 === 0) lineSize = 12;
454
+ else if (mmRounded % 5 === 0) lineSize = 10;
455
+ else lineSize = 6;
456
+ }
457
+ const left = isHorizontal ? position : this.options.ruleSize - lineSize;
458
+ const top = isHorizontal ? this.options.ruleSize - lineSize : position;
459
+ const width = isHorizontal ? 0 : lineSize;
460
+ const height = isHorizontal ? lineSize : 0;
461
+ darwLine(this.ctx, {
462
+ left,
463
+ top,
464
+ width,
465
+ height,
466
+ stroke: textColor.toRgb(),
467
+ });
468
+ }
469
+
470
+ // 标尺蓝色遮罩
471
+ if (this.objectRect) {
472
+ const axis = isHorizontal ? 'x' : 'y';
473
+ this.objectRect[axis].forEach((rect) => {
474
+ // 跳过指定矩形
475
+ if (rect.skip === axis) {
476
+ return;
477
+ }
478
+
479
+ // 获取数字的值
480
+ const getOriginValue = (type: 'start' | 'end') => {
481
+ if (this.unit === 'px') {
482
+ const value = this.options.editor.getSizeByUnit(
483
+ (type === 'start'
484
+ ? isHorizontal
485
+ ? rect.left
486
+ : rect.top
487
+ : isHorizontal
488
+ ? rect.left + rect.width
489
+ : rect.top + rect.height) /
490
+ zoom +
491
+ startCalibration
492
+ );
493
+ const num = Number(value);
494
+ if (Number.isNaN(num)) return undefined;
495
+ return Math.round(num * 100) / 100;
496
+ }
497
+ if (isHorizontal) {
498
+ return type === 'start' ? rect.originXStart : rect.originXEnd;
499
+ } else {
500
+ return type === 'start' ? rect.originYStart : rect.originYEnd;
501
+ }
502
+ };
503
+
504
+ const leftTextVal = getOriginValue('start');
505
+ const rightTextVal = getOriginValue('end');
506
+ const precision = this.options.editor.getPrecision?.() ?? 2;
507
+ const leftTextStr =
508
+ leftTextVal !== undefined ? formatFixedString(leftTextVal, precision) : undefined;
509
+ const rightTextStr =
510
+ rightTextVal !== undefined ? formatFixedString(rightTextVal, precision) : undefined;
511
+
512
+ const isSameText =
513
+ leftTextVal !== undefined &&
514
+ rightTextVal !== undefined &&
515
+ leftTextVal === rightTextVal;
516
+
517
+ // 背景遮罩
518
+ const maskOpt = {
519
+ isHorizontal,
520
+ width: isHorizontal ? 160 : this.options.ruleSize - 8,
521
+ height: isHorizontal ? this.options.ruleSize - 8 : 160,
522
+ backgroundColor: this.options.backgroundColor,
523
+ };
524
+ drawMask(this.ctx, {
525
+ ...maskOpt,
526
+ left: isHorizontal ? rect.left - 80 : 0,
527
+ top: isHorizontal ? 0 : rect.top - 80,
528
+ });
529
+ if (!isSameText) {
530
+ drawMask(this.ctx, {
531
+ ...maskOpt,
532
+ left: isHorizontal ? rect.width + rect.left - 80 : 0,
533
+ top: isHorizontal ? 0 : rect.height + rect.top - 80,
534
+ });
535
+ }
536
+
537
+ // 颜色
538
+ const highlightColor = new fabric.Color(
539
+ this.options.highlightColor
540
+ );
541
+
542
+ // 高亮遮罩
543
+ highlightColor.setAlpha(0.5);
544
+ darwRect(this.ctx, {
545
+ left: isHorizontal ? rect.left : this.options.ruleSize - 8,
546
+ top: isHorizontal ? this.options.ruleSize - 8 : rect.top,
547
+ width: isHorizontal ? rect.width : 8,
548
+ height: isHorizontal ? 8 : rect.height,
549
+ fill: highlightColor.toRgba(),
550
+ });
551
+
552
+ // 两边的数字
553
+ const pad =
554
+ this.options.ruleSize / 2 - this.options.fontSize / 2 - 4;
555
+
556
+ const textOpt = {
557
+ fill: highlightColor.toRgba(),
558
+ angle: isHorizontal ? 0 : -90,
559
+ };
560
+
561
+ if (leftTextStr !== undefined) {
562
+ darwText(this.ctx, {
563
+ ...textOpt,
564
+ text: leftTextStr,
565
+ left: isHorizontal ? rect.left - 2 : pad,
566
+ top: isHorizontal ? pad : rect.top - 2,
567
+ align: isSameText
568
+ ? 'center'
569
+ : isHorizontal
570
+ ? 'right'
571
+ : 'left',
572
+ });
573
+ }
574
+
575
+ if (!isSameText && rightTextStr !== undefined) {
576
+ darwText(this.ctx, {
577
+ ...textOpt,
578
+ text: rightTextStr,
579
+ left: isHorizontal ? rect.left + rect.width + 2 : pad,
580
+ top: isHorizontal ? pad : rect.top + rect.height + 2,
581
+ align: isHorizontal ? 'left' : 'right',
582
+ });
583
+ }
584
+
585
+ // 两边的线
586
+ const lineSize = isSameText ? 8 : 14;
587
+
588
+ highlightColor.setAlpha(1);
589
+
590
+ const lineOpt = {
591
+ width: isHorizontal ? 0 : lineSize,
592
+ height: isHorizontal ? lineSize : 0,
593
+ stroke: highlightColor.toRgba(),
594
+ };
595
+
596
+ darwLine(this.ctx, {
597
+ ...lineOpt,
598
+ left: isHorizontal
599
+ ? rect.left
600
+ : this.options.ruleSize - lineSize,
601
+ top: isHorizontal
602
+ ? this.options.ruleSize - lineSize
603
+ : rect.top,
604
+ });
605
+
606
+ if (!isSameText) {
607
+ darwLine(this.ctx, {
608
+ ...lineOpt,
609
+ left: isHorizontal
610
+ ? rect.left + rect.width
611
+ : this.options.ruleSize - lineSize,
612
+ top: isHorizontal
613
+ ? this.options.ruleSize - lineSize
614
+ : rect.top + rect.height,
615
+ });
616
+ }
617
+ });
618
+ }
619
+ // draw end
620
+ }
621
+
622
+ /**
623
+ * 计算起始点
624
+ */
625
+ // private calcCalibration() {
626
+ // if (this.startCalibration) return;
627
+ // // console.log('calcCalibration');
628
+ // const workspace = this.options.canvas.getObjects().find((item: any) => {
629
+ // return item.id === 'workspace';
630
+ // });
631
+ // if (!workspace) return;
632
+ // const rect = workspace.getBoundingRect(false);
633
+ // this.startCalibration = new fabric.Point(-rect.left, -rect.top).divide(this.getZoom());
634
+ // }
635
+
636
+ private calcObjectRect() {
637
+ const activeObjects = this.options.canvas.getActiveObjects();
638
+ if (activeObjects.length === 0) return;
639
+ const allRect = activeObjects.reduce((rects, obj) => {
640
+ const rect: HighlightRect = obj.getBoundingRect(false, true);
641
+ // 如果是分组单独计算坐标
642
+ if (obj.group) {
643
+ const baseGroup: any = obj.group;
644
+ const group = {
645
+ ...baseGroup,
646
+ scaleX: baseGroup?.scaleX ?? 1,
647
+ scaleY: baseGroup?.scaleY ?? 1,
648
+ } as any;
649
+ // 计算矩形坐标
650
+ rect.width *= group.scaleX;
651
+ rect.height *= group.scaleY;
652
+ const groupCenterX = group.width / 2 + group.left;
653
+ const objectOffsetFromCenterX =
654
+ (group.width / 2 + (obj.left ?? 0)) * (1 - group.scaleX);
655
+ rect.left +=
656
+ (groupCenterX - objectOffsetFromCenterX) * this.getZoom();
657
+ const groupCenterY = group.height / 2 + group.top;
658
+ const objectOffsetFromCenterY =
659
+ (group.height / 2 + (obj.top ?? 0)) * (1 - group.scaleY);
660
+ rect.top +=
661
+ (groupCenterY - objectOffsetFromCenterY) * this.getZoom();
662
+ }
663
+ if (obj instanceof fabric.GuideLine) {
664
+ rect.skip = obj.isHorizontal() ? 'x' : 'y';
665
+ }
666
+ if (this.unit !== 'px') {
667
+ const origin = (obj as any)._originSize?.[this.unit];
668
+ if (origin) {
669
+ if (obj instanceof fabric.Polygon && Array.isArray(origin.points)) {
670
+ const xs = origin.points.map((p: any) => p.x);
671
+ const ys = origin.points.map((p: any) => p.y);
672
+ const minX = Math.min(...xs);
673
+ const maxX = Math.max(...xs);
674
+ const minY = Math.min(...ys);
675
+ const maxY = Math.max(...ys);
676
+ rect.originXStart = minX;
677
+ rect.originXEnd = maxX;
678
+ rect.originYStart = minY;
679
+ rect.originYEnd = maxY;
680
+ } else {
681
+ const left = origin.left;
682
+ const top = origin.top;
683
+ let width = origin.width;
684
+ let height = origin.height;
685
+ if (width === undefined && origin.rx !== undefined) {
686
+ width = origin.rx * 2;
687
+ }
688
+ if (height === undefined && origin.ry !== undefined) {
689
+ height = origin.ry * 2;
690
+ }
691
+ if (left !== undefined && width !== undefined) {
692
+ rect.originXStart = left;
693
+ rect.originXEnd = left + width;
694
+ }
695
+ if (top !== undefined && height !== undefined) {
696
+ rect.originYStart = top;
697
+ rect.originYEnd = top + height;
698
+ }
699
+ }
700
+ }
701
+ }
702
+ rects.push(rect);
703
+ return rects;
704
+ }, [] as HighlightRect[]);
705
+ if (allRect.length === 0) return;
706
+ this.objectRect = {
707
+ x: mergeLines(allRect, true),
708
+ y: mergeLines(allRect, false),
709
+ };
710
+ }
711
+
712
+ private calcObjectRectFromTarget(target: fabric.Object) {
713
+ const getRectFromObj = (obj: fabric.Object): HighlightRect => {
714
+ const rect: HighlightRect = obj.getBoundingRect(false, true);
715
+ if ((obj as any).group) {
716
+ const baseGroup: any = (obj as any).group;
717
+ const group = {
718
+ ...baseGroup,
719
+ scaleX: baseGroup?.scaleX ?? 1,
720
+ scaleY: baseGroup?.scaleY ?? 1,
721
+ } as any;
722
+ rect.width *= group.scaleX;
723
+ rect.height *= group.scaleY;
724
+ const groupCenterX = group.width / 2 + group.left;
725
+ const objectOffsetFromCenterX =
726
+ (group.width / 2 + (obj.left ?? 0)) * (1 - group.scaleX);
727
+ rect.left +=
728
+ (groupCenterX - objectOffsetFromCenterX) * this.getZoom();
729
+ const groupCenterY = group.height / 2 + group.top;
730
+ const objectOffsetFromCenterY =
731
+ (group.height / 2 + (obj.top ?? 0)) * (1 - group.scaleY);
732
+ rect.top +=
733
+ (groupCenterY - objectOffsetFromCenterY) * this.getZoom();
734
+ }
735
+ if ((obj as any) instanceof fabric.GuideLine) {
736
+ (rect as any).skip = (obj as any).isHorizontal() ? 'x' : 'y';
737
+ }
738
+ if (this.unit !== 'px') {
739
+ const origin = (obj as any)._originSize?.[this.unit];
740
+ if (origin) {
741
+ if ((obj as any) instanceof fabric.Polygon && Array.isArray(origin.points)) {
742
+ const xs = origin.points.map((p: any) => p.x);
743
+ const ys = origin.points.map((p: any) => p.y);
744
+ const minX = Math.min(...xs);
745
+ const maxX = Math.max(...xs);
746
+ const minY = Math.min(...ys);
747
+ const maxY = Math.max(...ys);
748
+ rect.originXStart = minX;
749
+ rect.originXEnd = maxX;
750
+ rect.originYStart = minY;
751
+ rect.originYEnd = maxY;
752
+ } else {
753
+ const left = origin.left;
754
+ const top = origin.top;
755
+ let width = origin.width;
756
+ let height = origin.height;
757
+ if (width === undefined && origin.rx !== undefined) {
758
+ width = origin.rx * 2;
759
+ }
760
+ if (height === undefined && origin.ry !== undefined) {
761
+ height = origin.ry * 2;
762
+ }
763
+ if (left !== undefined && width !== undefined) {
764
+ rect.originXStart = left;
765
+ rect.originXEnd = left + width;
766
+ }
767
+ if (top !== undefined && height !== undefined) {
768
+ rect.originYStart = top;
769
+ rect.originYEnd = top + height;
770
+ }
771
+ }
772
+ }
773
+ }
774
+ return rect;
775
+ };
776
+
777
+ const rect = getRectFromObj(target);
778
+ this.objectRect = {
779
+ x: mergeLines([rect], true),
780
+ y: mergeLines([rect], false),
781
+ };
782
+ }
783
+
784
+ /**
785
+ * 清除起始点和矩形坐标
786
+ */
787
+ private clearStatus() {
788
+ // this.startCalibration = undefined;
789
+ this.objectRect = undefined;
790
+ }
791
+
792
+ /**
793
+ 判断鼠标是否在标尺上
794
+ * @param point
795
+ * @returns "vertical" | "horizontal" | false
796
+ */
797
+ public isPointOnRuler(point: Point) {
798
+ if (
799
+ new fabric.Rect({
800
+ left: 0,
801
+ top: 0,
802
+ width: this.options.ruleSize,
803
+ height: this.options.canvas.height,
804
+ }).containsPoint(point)
805
+ ) {
806
+ return 'vertical';
807
+ } else if (
808
+ new fabric.Rect({
809
+ left: 0,
810
+ top: 0,
811
+ width: this.options.canvas.width,
812
+ height: this.options.ruleSize,
813
+ }).containsPoint(point)
814
+ ) {
815
+ return 'horizontal';
816
+ }
817
+ return false;
818
+ }
819
+
820
+ private canvasMouseDown(e: IEvent<MouseEvent>) {
821
+ if (!e.pointer || !e.absolutePointer) return;
822
+ const hoveredRuler = this.isPointOnRuler(e.pointer);
823
+ if (hoveredRuler && this.activeOn === 'up') {
824
+ // 备份属性
825
+ this.lastAttr.selection = this.options.canvas.selection;
826
+ this.options.canvas.selection = false;
827
+ this.activeOn = 'down';
828
+
829
+ this.tempGuidelLine = new fabric.GuideLine(
830
+ hoveredRuler === 'horizontal'
831
+ ? e.absolutePointer.y
832
+ : e.absolutePointer.x,
833
+ {
834
+ axis: hoveredRuler,
835
+ visible: false,
836
+ }
837
+ );
838
+
839
+ this.options.canvas.add(this.tempGuidelLine);
840
+ this.options.canvas.setActiveObject(this.tempGuidelLine);
841
+
842
+ this.options.canvas._setupCurrentTransform(
843
+ e.e,
844
+ this.tempGuidelLine,
845
+ true
846
+ );
847
+
848
+ this.tempGuidelLine.fire('down', this.getCommonEventInfo(e));
849
+ }
850
+ }
851
+
852
+ private getCommonEventInfo = (e: IEvent<MouseEvent>) => {
853
+ if (!this.tempGuidelLine || !e.absolutePointer) return;
854
+ return {
855
+ e: e.e,
856
+ transform: this.tempGuidelLine.get('transform'),
857
+ pointer: {
858
+ x: e.absolutePointer.x,
859
+ y: e.absolutePointer.y,
860
+ },
861
+ target: this.tempGuidelLine,
862
+ };
863
+ };
864
+
865
+ private canvasMouseMove(e: IEvent<MouseEvent>) {
866
+ if (!e.pointer) return;
867
+
868
+ if (this.tempGuidelLine && e.absolutePointer) {
869
+ const pos: Partial<fabric.IGuideLineOptions> = {};
870
+ if (this.tempGuidelLine.axis === 'horizontal') {
871
+ pos.top = e.absolutePointer.y;
872
+ } else {
873
+ pos.left = e.absolutePointer.x;
874
+ }
875
+ this.tempGuidelLine.set({ ...pos, visible: true });
876
+
877
+ this.options.canvas.requestRenderAll();
878
+
879
+ const event = this.getCommonEventInfo(e);
880
+ this.options.canvas.fire('object:moving', event);
881
+ this.tempGuidelLine.fire('moving', event);
882
+ }
883
+
884
+ const hoveredRuler = this.isPointOnRuler(e.pointer);
885
+ if (!hoveredRuler) {
886
+ // 鼠标从里面出去
887
+ if (this.lastAttr.status !== 'out') {
888
+ // 更改鼠标指针
889
+ this.options.canvas.defaultCursor = this.lastAttr.cursor;
890
+ this.lastAttr.status = 'out';
891
+ }
892
+ return;
893
+ }
894
+ // const activeObjects = this.options.canvas.getActiveObjects();
895
+ // if (activeObjects.length === 1 && activeObjects[0] instanceof fabric.GuideLine) {
896
+ // return;
897
+ // }
898
+ // 鼠标从外边进入 或 在另一侧标尺
899
+ if (
900
+ this.lastAttr.status === 'out' ||
901
+ hoveredRuler !== this.lastAttr.status
902
+ ) {
903
+ // 更改鼠标指针
904
+ this.lastAttr.cursor = this.options.canvas.defaultCursor;
905
+ this.options.canvas.defaultCursor =
906
+ hoveredRuler === 'horizontal' ? 'ns-resize' : 'ew-resize';
907
+ this.lastAttr.status = hoveredRuler;
908
+ }
909
+ }
910
+
911
+ private canvasMouseUp(e: IEvent<MouseEvent>) {
912
+ if (this.activeOn !== 'down') return;
913
+
914
+ // 还原属性
915
+ this.options.canvas.selection = this.lastAttr.selection;
916
+ this.activeOn = 'up';
917
+
918
+ this.tempGuidelLine?.fire('up', this.getCommonEventInfo(e));
919
+
920
+ this.tempGuidelLine = undefined;
921
+ }
922
+ }
923
+
924
+ export default CanvasRuler;