@hprint/plugins 0.0.9-alpha.0 → 0.0.10

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,276 +1,309 @@
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
- type LayoutEntry = {
16
- object: fabric.Object;
17
- index: number;
18
- originalTop: number;
19
- originalBottom: number;
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
-
54
- applyActualContentLayout(templateContent: any, templateHeight: number) {
55
- const settings = (templateContent?.templateSettings ||
56
- {}) as ActualContentLayoutSettings;
57
- if (!settings.actualContentLayout) return templateHeight;
58
-
59
- const fabricObjects = this.canvas
60
- .getObjects()
61
- .filter(this.isPrintableObject);
62
- const mmPerPx = Number(this.editor.getSizeByUnit?.(1, 'mm')) || 1;
63
-
64
- fabricObjects.forEach((object) => this.preparePrintLayoutObject(object));
65
-
66
- const entries = fabricObjects.map((object, index) => {
67
- const origin = this.layoutOrigins.get(object);
68
- const originalTop =
69
- origin?.top ?? Number(object.top || 0) * mmPerPx;
70
- const originalHeight =
71
- origin?.height ??
72
- Number(object.getScaledHeight?.() || object.height || 0) *
73
- mmPerPx;
74
- return {
75
- object,
76
- index,
77
- 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
-
160
- private getOriginalTop(
161
- source: any,
162
- object: fabric.Object,
163
- mmPerPx: number
164
- ) {
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
171
- );
172
- }
173
-
174
- private getOriginalHeight(
175
- 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
189
- );
190
- }
191
-
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,
219
- });
220
- }
221
-
222
- private isPrintableObject(object: any) {
223
- return (
224
- object?.id !== 'workspace' &&
225
- object?.id !== 'coverMask' &&
226
- object?.type !== 'GuideLine'
227
- );
228
- }
229
-
230
- private isEmptyLayoutObject(object: any) {
231
- if (object?.visible === false) return true;
232
- const field = object?.extension?._field_;
233
- if (!field) return false;
234
- if (object.type === 'textbox') {
235
- return String(object.text ?? '') === '';
236
- }
237
- if (['barcode', 'qrcode'].includes(object.extensionType)) {
238
- return String(object.extension?.value ?? '') === '';
239
- }
240
- if (object.extensionType === 'imageTextList') {
241
- return (
242
- !Array.isArray(object.extension?.items) ||
243
- object.extension.items.length === 0
244
- );
245
- }
246
- return false;
247
- }
248
-
249
- private getActualLayoutHeight(object: any) {
250
- if (object?.extensionType !== 'imageTextList') {
251
- 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;
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
+ type LayoutEntry = {
16
+ object: fabric.Object;
17
+ index: number;
18
+ originalTop: number;
19
+ originalBottom: number;
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
+
54
+ applyActualContentLayout(templateContent: any, templateHeight: number) {
55
+ const settings = (templateContent?.templateSettings ||
56
+ {}) as ActualContentLayoutSettings;
57
+ if (!settings.actualContentLayout) return templateHeight;
58
+
59
+ const fabricObjects = this.canvas
60
+ .getObjects()
61
+ .filter(this.isPrintableObject);
62
+ const mmPerPx = Number(this.editor.getSizeByUnit?.(1, 'mm')) || 1;
63
+ const sourceObjects = this.getTemplateSourceObjects(templateContent);
64
+
65
+ fabricObjects.forEach((object) => this.preparePrintLayoutObject(object));
66
+
67
+ const entries = fabricObjects.map((object, index) => {
68
+ const origin = this.layoutOrigins.get(object);
69
+ const source = this.getSourceObject(object, sourceObjects);
70
+ const originalTop =
71
+ origin?.top ??
72
+ this.getOriginalTop(source, object, mmPerPx);
73
+ const originalHeight =
74
+ origin?.height ??
75
+ this.getOriginalHeight(source, object, mmPerPx);
76
+ return {
77
+ object,
78
+ index,
79
+ originalTop,
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
+
162
+ private getOriginalTop(
163
+ source: any,
164
+ object: fabric.Object,
165
+ mmPerPx: number
166
+ ) {
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
173
+ );
174
+ }
175
+
176
+ private getOriginalHeight(
177
+ source: any,
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
191
+ );
192
+ }
193
+
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);
200
+ });
201
+ return sourceObjects;
202
+ }
203
+
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}`);
215
+ }
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
+
255
+ private isEmptyLayoutObject(object: any) {
256
+ if (object?.visible === false) return true;
257
+ const field = object?.extension?._field_;
258
+ if (!field) return false;
259
+ if (object.type === 'textbox') {
260
+ return String(object.text ?? '') === '';
261
+ }
262
+ if (['barcode', 'qrcode'].includes(object.extensionType)) {
263
+ return (
264
+ String(object.extension?.value ?? '') === '' &&
265
+ !this.hasVisibleImageSource(object)
266
+ );
267
+ }
268
+ if (object.extensionType === 'imageTextList') {
269
+ return (
270
+ !Array.isArray(object.extension?.items) ||
271
+ object.extension.items.length === 0
272
+ );
273
+ }
274
+ return false;
275
+ }
276
+
277
+ private hasVisibleImageSource(object: any) {
278
+ const src = String(object?.getSrc?.() ?? object?.src ?? '');
279
+ return src !== '' && /^data:image\//i.test(src);
280
+ }
281
+
282
+ private getActualLayoutHeight(object: any) {
283
+ if (object?.extensionType !== 'imageTextList') {
284
+ return Number(
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;