@hprint/plugins 0.0.9-alpha.0 → 0.0.9-alpha.1

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.
@@ -1,56 +1,56 @@
1
- import { fabric } from '@hprint/core';
2
- import type { IEditor, IPluginTempl } from '@hprint/core';
3
-
4
- export interface ActualContentLayoutSettings {
5
- actualContentLayout?: boolean;
6
- overflowMode?: 'clip' | 'expand';
7
- }
8
-
9
- type IPlugin = Pick<ActualContentLayoutPlugin, 'applyActualContentLayout'>;
10
-
11
- declare module '@hprint/core' {
12
- interface IEditor extends IPlugin {}
13
- }
14
-
1
+ import { fabric } from '@hprint/core';
2
+ import type { IEditor, IPluginTempl } from '@hprint/core';
3
+
4
+ export interface ActualContentLayoutSettings {
5
+ actualContentLayout?: boolean;
6
+ overflowMode?: 'clip' | 'expand';
7
+ }
8
+
9
+ type IPlugin = Pick<ActualContentLayoutPlugin, 'applyActualContentLayout'>;
10
+
11
+ declare module '@hprint/core' {
12
+ interface IEditor extends IPlugin {}
13
+ }
14
+
15
15
  type LayoutEntry = {
16
16
  object: fabric.Object;
17
17
  index: number;
18
18
  originalTop: number;
19
19
  originalBottom: number;
20
20
  };
21
-
22
- class ActualContentLayoutPlugin implements IPluginTempl {
23
- static pluginName = 'ActualContentLayoutPlugin';
24
- static apis = ['applyActualContentLayout'];
25
-
26
- constructor(
27
- public canvas: fabric.Canvas,
28
- public editor: IEditor
29
- ) {}
30
-
31
- private layoutOrigins = new WeakMap<
32
- fabric.Object,
33
- { top: number; height: number }
34
- >();
35
-
36
- async hookTransformObjectEnd(...args: unknown[]) {
37
- const { originObject, fabricObject } = args[0] as {
38
- originObject: any;
39
- fabricObject: fabric.Object;
40
- };
41
- if (!this.isPrintableObject(originObject)) return;
42
-
43
- const mmPerPx = Number(this.editor.getSizeByUnit?.(1, 'mm')) || 1;
44
- this.layoutOrigins.set(fabricObject, {
45
- top: this.getOriginalTop(originObject, fabricObject, mmPerPx),
46
- height: this.getOriginalHeight(
47
- originObject,
48
- fabricObject,
49
- mmPerPx
50
- ),
51
- });
52
- }
53
-
21
+
22
+ class ActualContentLayoutPlugin implements IPluginTempl {
23
+ static pluginName = 'ActualContentLayoutPlugin';
24
+ static apis = ['applyActualContentLayout'];
25
+
26
+ constructor(
27
+ public canvas: fabric.Canvas,
28
+ public editor: IEditor
29
+ ) {}
30
+
31
+ private layoutOrigins = new WeakMap<
32
+ fabric.Object,
33
+ { top: number; height: number }
34
+ >();
35
+
36
+ async hookTransformObjectEnd(...args: unknown[]) {
37
+ const { originObject, fabricObject } = args[0] as {
38
+ originObject: any;
39
+ fabricObject: fabric.Object;
40
+ };
41
+ if (!this.isPrintableObject(originObject)) return;
42
+
43
+ const mmPerPx = Number(this.editor.getSizeByUnit?.(1, 'mm')) || 1;
44
+ this.layoutOrigins.set(fabricObject, {
45
+ top: this.getOriginalTop(originObject, fabricObject, mmPerPx),
46
+ height: this.getOriginalHeight(
47
+ originObject,
48
+ fabricObject,
49
+ mmPerPx
50
+ ),
51
+ });
52
+ }
53
+
54
54
  applyActualContentLayout(templateContent: any, templateHeight: number) {
55
55
  const settings = (templateContent?.templateSettings ||
56
56
  {}) as ActualContentLayoutSettings;
@@ -60,173 +60,198 @@ class ActualContentLayoutPlugin implements IPluginTempl {
60
60
  .getObjects()
61
61
  .filter(this.isPrintableObject);
62
62
  const mmPerPx = Number(this.editor.getSizeByUnit?.(1, 'mm')) || 1;
63
+ const sourceObjects = this.getTemplateSourceObjects(templateContent);
63
64
 
64
65
  fabricObjects.forEach((object) => this.preparePrintLayoutObject(object));
65
66
 
66
67
  const entries = fabricObjects.map((object, index) => {
67
68
  const origin = this.layoutOrigins.get(object);
69
+ const source = this.getSourceObject(object, sourceObjects);
68
70
  const originalTop =
69
- origin?.top ?? Number(object.top || 0) * mmPerPx;
71
+ origin?.top ??
72
+ this.getOriginalTop(source, object, mmPerPx);
70
73
  const originalHeight =
71
74
  origin?.height ??
72
- Number(object.getScaledHeight?.() || object.height || 0) *
73
- mmPerPx;
75
+ this.getOriginalHeight(source, object, mmPerPx);
74
76
  return {
75
77
  object,
76
78
  index,
77
79
  originalTop,
78
- originalBottom: originalTop + originalHeight,
79
- };
80
- }) as LayoutEntry[];
81
-
82
- entries.sort(
83
- (a, b) => a.originalTop - b.originalTop || a.index - b.index
84
- );
85
- const rows: LayoutEntry[][] = [];
86
- const sameRowTolerance = 0.01;
87
- entries.forEach((entry) => {
88
- const row = rows[rows.length - 1];
89
- if (
90
- row &&
91
- Math.abs(row[0].originalTop - entry.originalTop) <=
92
- sameRowTolerance
93
- ) {
94
- row.push(entry);
95
- } else {
96
- rows.push([entry]);
97
- }
98
- });
99
-
100
- let previousOriginalBottom: number | undefined;
101
- let previousActualBottom: number | undefined;
102
- let pendingGap = 0;
103
- let lastVisibleOriginalBottom = 0;
104
-
105
- rows.forEach((row) => {
106
- const rowOriginalTop = row[0].originalTop;
107
- const rowOriginalBottom = Math.max(
108
- ...row.map((entry) => entry.originalBottom)
109
- );
110
- if (previousOriginalBottom !== undefined) {
111
- pendingGap += Math.max(
112
- 0,
113
- rowOriginalTop - previousOriginalBottom
114
- );
115
- }
116
- previousOriginalBottom = rowOriginalBottom;
117
-
118
- const visibleEntries = row.filter(
119
- (entry) => !this.isEmptyLayoutObject(entry.object)
120
- );
121
- if (!visibleEntries.length) return;
122
-
123
- const targetTop =
124
- previousActualBottom === undefined
125
- ? rowOriginalTop
126
- : previousActualBottom + pendingGap;
127
- visibleEntries.forEach((entry) => {
128
- entry.object.set('top', targetTop / mmPerPx);
129
- entry.object.setCoords();
130
- });
131
- previousActualBottom = Math.max(
132
- ...visibleEntries.map(
133
- (entry) =>
134
- (Number(entry.object.top || 0) +
135
- this.getActualLayoutHeight(entry.object)) *
136
- mmPerPx
137
- )
138
- );
139
- lastVisibleOriginalBottom = rowOriginalBottom;
140
- pendingGap = 0;
141
- });
142
-
143
- this.canvas.requestRenderAll();
144
- if (
145
- settings.overflowMode !== 'expand' ||
146
- previousActualBottom === undefined
147
- ) {
148
- return templateHeight;
149
- }
150
- const bottomSpace = Math.max(
151
- 0,
152
- templateHeight - lastVisibleOriginalBottom
153
- );
154
- return Math.max(
155
- templateHeight,
156
- previousActualBottom + bottomSpace
157
- );
158
- }
159
-
80
+ originalBottom: originalTop + originalHeight,
81
+ };
82
+ }) as LayoutEntry[];
83
+
84
+ entries.sort(
85
+ (a, b) => a.originalTop - b.originalTop || a.index - b.index
86
+ );
87
+ const rows: LayoutEntry[][] = [];
88
+ const sameRowTolerance = 0.01;
89
+ entries.forEach((entry) => {
90
+ const row = rows[rows.length - 1];
91
+ if (
92
+ row &&
93
+ Math.abs(row[0].originalTop - entry.originalTop) <=
94
+ sameRowTolerance
95
+ ) {
96
+ row.push(entry);
97
+ } else {
98
+ rows.push([entry]);
99
+ }
100
+ });
101
+
102
+ let previousOriginalBottom: number | undefined;
103
+ let previousActualBottom: number | undefined;
104
+ let pendingGap = 0;
105
+ let lastVisibleOriginalBottom = 0;
106
+
107
+ rows.forEach((row) => {
108
+ const rowOriginalTop = row[0].originalTop;
109
+ const rowOriginalBottom = Math.max(
110
+ ...row.map((entry) => entry.originalBottom)
111
+ );
112
+ if (previousOriginalBottom !== undefined) {
113
+ pendingGap += Math.max(
114
+ 0,
115
+ rowOriginalTop - previousOriginalBottom
116
+ );
117
+ }
118
+ previousOriginalBottom = rowOriginalBottom;
119
+
120
+ const visibleEntries = row.filter(
121
+ (entry) => !this.isEmptyLayoutObject(entry.object)
122
+ );
123
+ if (!visibleEntries.length) return;
124
+
125
+ const targetTop =
126
+ previousActualBottom === undefined
127
+ ? rowOriginalTop
128
+ : previousActualBottom + pendingGap;
129
+ visibleEntries.forEach((entry) => {
130
+ entry.object.set('top', targetTop / mmPerPx);
131
+ entry.object.setCoords();
132
+ });
133
+ previousActualBottom = Math.max(
134
+ ...visibleEntries.map(
135
+ (entry) =>
136
+ (Number(entry.object.top || 0) +
137
+ this.getActualLayoutHeight(entry.object)) *
138
+ mmPerPx
139
+ )
140
+ );
141
+ lastVisibleOriginalBottom = rowOriginalBottom;
142
+ pendingGap = 0;
143
+ });
144
+
145
+ this.canvas.requestRenderAll();
146
+ if (
147
+ settings.overflowMode !== 'expand' ||
148
+ previousActualBottom === undefined
149
+ ) {
150
+ return templateHeight;
151
+ }
152
+ const bottomSpace = Math.max(
153
+ 0,
154
+ templateHeight - lastVisibleOriginalBottom
155
+ );
156
+ return Math.max(
157
+ templateHeight,
158
+ previousActualBottom + bottomSpace
159
+ );
160
+ }
161
+
160
162
  private getOriginalTop(
161
163
  source: any,
162
164
  object: fabric.Object,
163
165
  mmPerPx: number
164
166
  ) {
165
- const top = Number(source?.top);
166
- if (Number.isFinite(top)) return top * mmPerPx;
167
- return this.getOriginMmValue(
168
- source,
169
- 'top',
170
- Number(object.top || 0) * mmPerPx
167
+ const top = Number(source?.top);
168
+ if (Number.isFinite(top)) return top * mmPerPx;
169
+ return this.getOriginMmValue(
170
+ source,
171
+ 'top',
172
+ Number(object.top || 0) * mmPerPx
171
173
  );
172
174
  }
173
175
 
174
176
  private getOriginalHeight(
175
177
  source: any,
176
- object: fabric.Object,
177
- mmPerPx: number
178
- ) {
179
- const height = Number(source?.height);
180
- const scaleY = Number(source?.scaleY ?? 1);
181
- if (Number.isFinite(height) && Number.isFinite(scaleY)) {
182
- return Math.abs(height * scaleY) * mmPerPx;
183
- }
184
- return this.getOriginMmValue(
185
- source,
186
- 'height',
187
- Number(object.getScaledHeight?.() || object.height || 0) *
188
- mmPerPx
178
+ object: fabric.Object,
179
+ mmPerPx: number
180
+ ) {
181
+ const height = Number(source?.height);
182
+ const scaleY = Number(source?.scaleY ?? 1);
183
+ if (Number.isFinite(height) && Number.isFinite(scaleY)) {
184
+ return Math.abs(height * scaleY) * mmPerPx;
185
+ }
186
+ return this.getOriginMmValue(
187
+ source,
188
+ 'height',
189
+ Number(object.getScaledHeight?.() || object.height || 0) *
190
+ mmPerPx
189
191
  );
190
192
  }
191
193
 
192
- private getOriginMmValue(
193
- source: any,
194
- field: 'top' | 'height',
195
- fallback: number
196
- ) {
197
- const originalValue = source?._originSize?.mm?.[field];
198
- if (
199
- originalValue === undefined ||
200
- originalValue === null ||
201
- originalValue === ''
202
- ) {
203
- return fallback;
204
- }
205
- const value = Number(originalValue);
206
- return Number.isFinite(value) ? value : fallback;
207
- }
208
-
209
- private preparePrintLayoutObject(object: any) {
210
- if (object?.extensionType !== 'imageTextList') return;
211
- if (object.extension?._clipContent === true) return;
212
-
213
- // Design canvases can opt into clipping, but print/layout canvases should
214
- // measure and render the natural image-text content height.
215
- if (object.clipPath) object.set('clipPath', undefined);
216
- object.set({
217
- objectCaching: false,
218
- dirty: true,
194
+ private getTemplateSourceObjects(templateContent: any) {
195
+ const sourceObjects = new Map<string, any>();
196
+ if (!Array.isArray(templateContent?.objects)) return sourceObjects;
197
+ templateContent.objects.forEach((source: any, index: number) => {
198
+ if (source?.id) sourceObjects.set(`id:${source.id}`, source);
199
+ sourceObjects.set(`index:${index}`, source);
219
200
  });
201
+ return sourceObjects;
220
202
  }
221
203
 
222
- private isPrintableObject(object: any) {
223
- return (
224
- object?.id !== 'workspace' &&
225
- object?.id !== 'coverMask' &&
226
- object?.type !== 'GuideLine'
227
- );
204
+ private getSourceObject(
205
+ object: any,
206
+ sourceObjects: Map<string, any>
207
+ ) {
208
+ if (object?.id) {
209
+ const source = sourceObjects.get(`id:${object.id}`);
210
+ if (source) return source;
211
+ }
212
+ const objects = this.canvas.getObjects().filter(this.isPrintableObject);
213
+ const index = objects.indexOf(object);
214
+ return sourceObjects.get(`index:${index}`);
228
215
  }
229
-
216
+
217
+ private getOriginMmValue(
218
+ source: any,
219
+ field: 'top' | 'height',
220
+ fallback: number
221
+ ) {
222
+ const originalValue = source?._originSize?.mm?.[field];
223
+ if (
224
+ originalValue === undefined ||
225
+ originalValue === null ||
226
+ originalValue === ''
227
+ ) {
228
+ return fallback;
229
+ }
230
+ const value = Number(originalValue);
231
+ return Number.isFinite(value) ? value : fallback;
232
+ }
233
+
234
+ private preparePrintLayoutObject(object: any) {
235
+ if (object?.extensionType !== 'imageTextList') return;
236
+ if (object.extension?._clipContent === true) return;
237
+
238
+ // Design canvases can opt into clipping, but print/layout canvases should
239
+ // measure and render the natural image-text content height.
240
+ if (object.clipPath) object.set('clipPath', undefined);
241
+ object.set({
242
+ objectCaching: false,
243
+ dirty: true,
244
+ });
245
+ }
246
+
247
+ private isPrintableObject(object: any) {
248
+ return (
249
+ object?.id !== 'workspace' &&
250
+ object?.id !== 'coverMask' &&
251
+ object?.type !== 'GuideLine'
252
+ );
253
+ }
254
+
230
255
  private isEmptyLayoutObject(object: any) {
231
256
  if (object?.visible === false) return true;
232
257
  const field = object?.extension?._field_;
@@ -235,42 +260,50 @@ class ActualContentLayoutPlugin implements IPluginTempl {
235
260
  return String(object.text ?? '') === '';
236
261
  }
237
262
  if (['barcode', 'qrcode'].includes(object.extensionType)) {
238
- return String(object.extension?.value ?? '') === '';
263
+ return (
264
+ String(object.extension?.value ?? '') === '' &&
265
+ !this.hasVisibleImageSource(object)
266
+ );
239
267
  }
240
268
  if (object.extensionType === 'imageTextList') {
241
269
  return (
242
270
  !Array.isArray(object.extension?.items) ||
243
- object.extension.items.length === 0
244
- );
271
+ object.extension.items.length === 0
272
+ );
245
273
  }
246
274
  return false;
247
275
  }
248
276
 
277
+ private hasVisibleImageSource(object: any) {
278
+ const src = String(object?.getSrc?.() ?? object?.src ?? '');
279
+ return src !== '' && /^data:image\//i.test(src);
280
+ }
281
+
249
282
  private getActualLayoutHeight(object: any) {
250
283
  if (object?.extensionType !== 'imageTextList') {
251
284
  return Number(
252
- object.getScaledHeight?.() || object.height || 0
253
- );
254
- }
255
-
256
- const children = object.getObjects?.() || object._objects || [];
257
- const contentChildren = children.slice(1);
258
- if (!contentChildren.length) return 0;
259
-
260
- const groupHeight = Number(object.height || 0);
261
- const groupScaleY = Math.abs(Number(object.scaleY ?? 1));
262
- const contentBottom = Math.max(
263
- ...contentChildren.map(
264
- (child: any) =>
265
- Number(child.top || 0) +
266
- groupHeight / 2 +
267
- Number(
268
- child.getScaledHeight?.() || child.height || 0
269
- )
270
- )
271
- );
272
- return Math.max(0, contentBottom) * groupScaleY;
273
- }
274
- }
275
-
276
- export default ActualContentLayoutPlugin;
285
+ object.getScaledHeight?.() || object.height || 0
286
+ );
287
+ }
288
+
289
+ const children = object.getObjects?.() || object._objects || [];
290
+ const contentChildren = children.slice(1);
291
+ if (!contentChildren.length) return 0;
292
+
293
+ const groupHeight = Number(object.height || 0);
294
+ const groupScaleY = Math.abs(Number(object.scaleY ?? 1));
295
+ const contentBottom = Math.max(
296
+ ...contentChildren.map(
297
+ (child: any) =>
298
+ Number(child.top || 0) +
299
+ groupHeight / 2 +
300
+ Number(
301
+ child.getScaledHeight?.() || child.height || 0
302
+ )
303
+ )
304
+ );
305
+ return Math.max(0, contentBottom) * groupScaleY;
306
+ }
307
+ }
308
+
309
+ export default ActualContentLayoutPlugin;
@@ -86,7 +86,11 @@ class HistoryPlugin implements IPluginTempl {
86
86
  // 加载状态
87
87
  private _loadState(state: string, eventName: string, callback?: callback) {
88
88
  this.isLoading = true;
89
- this.isProcessing = true;
89
+ this.isProcessing = true;
90
+ const activeIds = this.canvas
91
+ .getActiveObjects()
92
+ .map((object: any) => object?.id)
93
+ .filter(Boolean);
90
94
 
91
95
  // 处理 workspace 的特殊情况
92
96
  const parsedState = JSON.parse(state);
@@ -97,17 +101,53 @@ class HistoryPlugin implements IPluginTempl {
97
101
  workspace.evented = false;
98
102
  }
99
103
 
100
- this.canvas.loadFromJSON(state, () => {
101
- this.canvas.renderAll();
102
- this.canvas.fire(eventName);
103
- this.isProcessing = false;
104
- this.isLoading = false;
105
- callback?.();
106
- });
107
- }
108
-
109
- // 获取历史记录状态
110
- private getState() {
104
+ const hookPromises: Promise<void>[] = [];
105
+ this.canvas.loadFromJSON(
106
+ state,
107
+ () => {
108
+ Promise.all(hookPromises).then(() => {
109
+ this.restoreActiveObjects(activeIds);
110
+ this.canvas.renderAll();
111
+ this.canvas.fire(eventName);
112
+ this.isProcessing = false;
113
+ this.isLoading = false;
114
+ callback?.();
115
+ });
116
+ },
117
+ (originObject: any, fabricObject: fabric.Object) => {
118
+ hookPromises.push(
119
+ new Promise((resolve) => {
120
+ this.editor.hooksEntity.hookTransformObjectEnd.callAsync(
121
+ { originObject, fabricObject },
122
+ () => resolve()
123
+ );
124
+ })
125
+ );
126
+ }
127
+ );
128
+ }
129
+
130
+ private restoreActiveObjects(activeIds: string[]) {
131
+ this.canvas.discardActiveObject();
132
+ if (!activeIds.length) return;
133
+
134
+ const objects = this.canvas
135
+ .getObjects()
136
+ .filter((object: any) => activeIds.includes(object?.id));
137
+ if (objects.length === 1) {
138
+ this.canvas.setActiveObject(objects[0]);
139
+ return;
140
+ }
141
+ if (objects.length > 1) {
142
+ const selection = new fabric.ActiveSelection(objects, {
143
+ canvas: this.canvas,
144
+ });
145
+ this.canvas.setActiveObject(selection);
146
+ }
147
+ }
148
+
149
+ // 获取历史记录状态
150
+ private getState() {
111
151
  return {
112
152
  undoCount: this.currentIndex - 1,
113
153
  redoCount: this.stack.length - this.currentIndex,