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

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