@pooder/kit 6.1.1 → 6.2.0

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 +1,165 @@
1
- export type { DielineGeometry, DielineState, LineStyle } from "./DielineTool";
1
+ import type { ConfigurationService } from "@pooder/core";
2
+ import { parseLengthToMm } from "../../units";
3
+ import {
4
+ DEFAULT_DIELINE_SHAPE,
5
+ DEFAULT_DIELINE_SHAPE_STYLE,
6
+ normalizeShapeStyle,
7
+ normalizeDielineShape,
8
+ } from "../dielineShape";
9
+ import type { DielineShape, DielineShapeStyle } from "../dielineShape";
10
+ import { readSizeState } from "../../shared/scene/sceneLayoutModel";
11
+ import type { DielineFeature } from "../geometry";
12
+
13
+ export interface DielineGeometry {
14
+ shape: DielineShape;
15
+ shapeStyle: DielineShapeStyle;
16
+ unit: "px";
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ radius: number;
22
+ offset: number;
23
+ borderLength?: number;
24
+ scale?: number;
25
+ strokeWidth?: number;
26
+ pathData?: string;
27
+ customSourceWidthPx?: number;
28
+ customSourceHeightPx?: number;
29
+ }
30
+
31
+ export interface LineStyle {
32
+ width: number;
33
+ color: string;
34
+ dashLength: number;
35
+ style: "solid" | "dashed" | "hidden";
36
+ }
37
+
38
+ export interface DielineState {
39
+ shape: DielineShape;
40
+ shapeStyle: DielineShapeStyle;
41
+ width: number;
42
+ height: number;
43
+ radius: number;
44
+ offset: number;
45
+ padding: number | string;
46
+ mainLine: LineStyle;
47
+ offsetLine: LineStyle;
48
+ insideColor: string;
49
+ showBleedLines: boolean;
50
+ features: DielineFeature[];
51
+ pathData?: string;
52
+ customSourceWidthPx?: number;
53
+ customSourceHeightPx?: number;
54
+ }
55
+
56
+ export function createDefaultDielineState(): DielineState {
57
+ return {
58
+ shape: DEFAULT_DIELINE_SHAPE,
59
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
60
+ width: 500,
61
+ height: 500,
62
+ radius: 0,
63
+ offset: 0,
64
+ padding: 140,
65
+ mainLine: {
66
+ width: 2.7,
67
+ color: "#FF0000",
68
+ dashLength: 5,
69
+ style: "solid",
70
+ },
71
+ offsetLine: {
72
+ width: 2.7,
73
+ color: "#FF0000",
74
+ dashLength: 5,
75
+ style: "solid",
76
+ },
77
+ insideColor: "rgba(0,0,0,0)",
78
+ showBleedLines: true,
79
+ features: [],
80
+ };
81
+ }
82
+
83
+ export function readDielineState(
84
+ configService: ConfigurationService,
85
+ fallback?: Partial<DielineState>,
86
+ ): DielineState {
87
+ const base = createDefaultDielineState();
88
+ if (fallback) {
89
+ Object.assign(base, fallback);
90
+ if (fallback.mainLine) {
91
+ base.mainLine = { ...base.mainLine, ...fallback.mainLine };
92
+ }
93
+ if (fallback.offsetLine) {
94
+ base.offsetLine = { ...base.offsetLine, ...fallback.offsetLine };
95
+ }
96
+ if (fallback.shapeStyle) {
97
+ base.shapeStyle = normalizeShapeStyle(fallback.shapeStyle, base.shapeStyle);
98
+ }
99
+ }
100
+
101
+ const sizeState = readSizeState(configService);
102
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
103
+ const sourceHeight = Number(
104
+ configService.get("dieline.customSourceHeightPx", 0),
105
+ );
106
+
107
+ return {
108
+ ...base,
109
+ shape: normalizeDielineShape(
110
+ configService.get("dieline.shape", base.shape),
111
+ base.shape,
112
+ ),
113
+ shapeStyle: normalizeShapeStyle(
114
+ configService.get("dieline.shapeStyle", base.shapeStyle),
115
+ base.shapeStyle,
116
+ ),
117
+ width: sizeState.actualWidthMm,
118
+ height: sizeState.actualHeightMm,
119
+ radius: parseLengthToMm(configService.get("dieline.radius", base.radius), "mm"),
120
+ padding: sizeState.viewPadding,
121
+ offset:
122
+ sizeState.cutMode === "outset"
123
+ ? sizeState.cutMarginMm
124
+ : sizeState.cutMode === "inset"
125
+ ? -sizeState.cutMarginMm
126
+ : 0,
127
+ mainLine: {
128
+ width: configService.get("dieline.strokeWidth", base.mainLine.width),
129
+ color: configService.get("dieline.strokeColor", base.mainLine.color),
130
+ dashLength: configService.get(
131
+ "dieline.dashLength",
132
+ base.mainLine.dashLength,
133
+ ),
134
+ style: configService.get("dieline.style", base.mainLine.style),
135
+ },
136
+ offsetLine: {
137
+ width: configService.get(
138
+ "dieline.offsetStrokeWidth",
139
+ base.offsetLine.width,
140
+ ),
141
+ color: configService.get(
142
+ "dieline.offsetStrokeColor",
143
+ base.offsetLine.color,
144
+ ),
145
+ dashLength: configService.get(
146
+ "dieline.offsetDashLength",
147
+ base.offsetLine.dashLength,
148
+ ),
149
+ style: configService.get("dieline.offsetStyle", base.offsetLine.style),
150
+ },
151
+ insideColor: configService.get("dieline.insideColor", base.insideColor),
152
+ showBleedLines: configService.get(
153
+ "dieline.showBleedLines",
154
+ base.showBleedLines,
155
+ ),
156
+ features: configService.get("dieline.features", base.features),
157
+ pathData: configService.get("dieline.pathData", base.pathData),
158
+ customSourceWidthPx:
159
+ Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : undefined,
160
+ customSourceHeightPx:
161
+ Number.isFinite(sourceHeight) && sourceHeight > 0
162
+ ? sourceHeight
163
+ : undefined,
164
+ };
165
+ }
@@ -0,0 +1,275 @@
1
+ import type { Pattern } from "fabric";
2
+ import type { RenderEffectSpec, RenderObjectSpec, VisibilityExpr } from "../../services";
3
+ import type { SceneLayoutSnapshot } from "../../shared/scene/sceneLayoutModel";
4
+ import { generateBleedZonePath, generateDielinePath } from "../geometry";
5
+ import { IMAGE_OBJECT_LAYER_ID } from "../../shared/constants/layers";
6
+ import type { DielineState } from "./model";
7
+
8
+ interface DielineRenderIds {
9
+ inside: string;
10
+ bleedZone: string;
11
+ offsetBorder: string;
12
+ border: string;
13
+ clip: string;
14
+ clipSource: string;
15
+ }
16
+
17
+ export interface DielineRenderBundle {
18
+ specs: RenderObjectSpec[];
19
+ effects: RenderEffectSpec[];
20
+ }
21
+
22
+ export interface DielineRenderOptions {
23
+ state: DielineState;
24
+ sceneLayout: SceneLayoutSnapshot;
25
+ canvasWidth: number;
26
+ canvasHeight: number;
27
+ hasImages: boolean;
28
+ ids?: Partial<DielineRenderIds>;
29
+ createHatchPattern?: (color: string) => Pattern | undefined;
30
+ includeImageClipEffect?: boolean;
31
+ clipTargetPassIds?: string[];
32
+ clipVisibility?: VisibilityExpr;
33
+ }
34
+
35
+ const DEFAULT_IDS: DielineRenderIds = {
36
+ inside: "dieline.inside",
37
+ bleedZone: "dieline.bleed-zone",
38
+ offsetBorder: "dieline.offset-border",
39
+ border: "dieline.border",
40
+ clip: "dieline.clip.image",
41
+ clipSource: "dieline.effect.clip-path",
42
+ };
43
+
44
+ function scaleFeatures(state: DielineState, scale: number) {
45
+ return (state.features || []).map((feature) => ({
46
+ ...feature,
47
+ x: feature.x,
48
+ y: feature.y,
49
+ width: (feature.width || 0) * scale,
50
+ height: (feature.height || 0) * scale,
51
+ radius: (feature.radius || 0) * scale,
52
+ }));
53
+ }
54
+
55
+ export function buildDielineRenderBundle(
56
+ options: DielineRenderOptions,
57
+ ): DielineRenderBundle {
58
+ const ids = { ...DEFAULT_IDS, ...(options.ids || {}) };
59
+ const {
60
+ state,
61
+ sceneLayout,
62
+ canvasWidth,
63
+ canvasHeight,
64
+ hasImages,
65
+ createHatchPattern,
66
+ includeImageClipEffect = true,
67
+ clipTargetPassIds = [IMAGE_OBJECT_LAYER_ID],
68
+ clipVisibility,
69
+ } = options;
70
+ const { shape, shapeStyle, radius, mainLine, offsetLine, insideColor } = state;
71
+
72
+ const scale = sceneLayout.scale;
73
+ const cx = sceneLayout.trimRect.centerX;
74
+ const cy = sceneLayout.trimRect.centerY;
75
+ const visualWidth = sceneLayout.trimRect.width;
76
+ const visualHeight = sceneLayout.trimRect.height;
77
+ const visualRadius = radius * scale;
78
+ const cutW = sceneLayout.cutRect.width;
79
+ const cutH = sceneLayout.cutRect.height;
80
+ const visualOffset = (cutW - visualWidth) / 2;
81
+ const cutR =
82
+ visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
83
+ const absoluteFeatures = scaleFeatures(state, scale);
84
+ const cutFeatures = absoluteFeatures.filter((feature) => !feature.skipCut);
85
+
86
+ const common = {
87
+ shape,
88
+ shapeStyle,
89
+ pathData: state.pathData,
90
+ customSourceWidthPx: state.customSourceWidthPx,
91
+ customSourceHeightPx: state.customSourceHeightPx,
92
+ canvasWidth,
93
+ canvasHeight,
94
+ };
95
+
96
+ const specs: RenderObjectSpec[] = [];
97
+
98
+ if (
99
+ insideColor &&
100
+ insideColor !== "transparent" &&
101
+ insideColor !== "rgba(0,0,0,0)" &&
102
+ !hasImages
103
+ ) {
104
+ specs.push({
105
+ id: ids.inside,
106
+ type: "path",
107
+ space: "screen",
108
+ data: { id: ids.inside, type: "dieline" },
109
+ props: {
110
+ pathData: generateDielinePath({
111
+ ...common,
112
+ width: cutW,
113
+ height: cutH,
114
+ radius: cutR,
115
+ x: cx,
116
+ y: cy,
117
+ features: cutFeatures,
118
+ }),
119
+ fill: insideColor,
120
+ stroke: null,
121
+ selectable: false,
122
+ evented: false,
123
+ originX: "left",
124
+ originY: "top",
125
+ },
126
+ });
127
+ }
128
+
129
+ if (Math.abs(visualOffset) > 0.0001) {
130
+ const trimPathInput = {
131
+ ...common,
132
+ width: visualWidth,
133
+ height: visualHeight,
134
+ radius: visualRadius,
135
+ x: cx,
136
+ y: cy,
137
+ features: cutFeatures,
138
+ };
139
+ const cutPathInput = {
140
+ ...common,
141
+ width: cutW,
142
+ height: cutH,
143
+ radius: cutR,
144
+ x: cx,
145
+ y: cy,
146
+ features: cutFeatures,
147
+ };
148
+
149
+ if (state.showBleedLines !== false) {
150
+ const pattern = createHatchPattern?.(mainLine.color);
151
+ if (pattern) {
152
+ specs.push({
153
+ id: ids.bleedZone,
154
+ type: "path",
155
+ space: "screen",
156
+ data: { id: ids.bleedZone, type: "dieline" },
157
+ props: {
158
+ pathData: generateBleedZonePath(
159
+ trimPathInput,
160
+ cutPathInput,
161
+ visualOffset,
162
+ ),
163
+ fill: pattern,
164
+ stroke: null,
165
+ selectable: false,
166
+ evented: false,
167
+ objectCaching: false,
168
+ originX: "left",
169
+ originY: "top",
170
+ },
171
+ });
172
+ }
173
+ }
174
+
175
+ specs.push({
176
+ id: ids.offsetBorder,
177
+ type: "path",
178
+ space: "screen",
179
+ data: { id: ids.offsetBorder, type: "dieline" },
180
+ props: {
181
+ pathData: generateDielinePath(cutPathInput),
182
+ fill: null,
183
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
184
+ strokeWidth: offsetLine.width,
185
+ strokeDashArray:
186
+ offsetLine.style === "dashed"
187
+ ? [offsetLine.dashLength, offsetLine.dashLength]
188
+ : undefined,
189
+ selectable: false,
190
+ evented: false,
191
+ originX: "left",
192
+ originY: "top",
193
+ },
194
+ });
195
+ }
196
+
197
+ specs.push({
198
+ id: ids.border,
199
+ type: "path",
200
+ space: "screen",
201
+ data: { id: ids.border, type: "dieline" },
202
+ props: {
203
+ pathData: generateDielinePath({
204
+ ...common,
205
+ width: visualWidth,
206
+ height: visualHeight,
207
+ radius: visualRadius,
208
+ x: cx,
209
+ y: cy,
210
+ features: absoluteFeatures,
211
+ }),
212
+ fill: "transparent",
213
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
214
+ strokeWidth: mainLine.width,
215
+ strokeDashArray:
216
+ mainLine.style === "dashed"
217
+ ? [mainLine.dashLength, mainLine.dashLength]
218
+ : undefined,
219
+ selectable: false,
220
+ evented: false,
221
+ originX: "left",
222
+ originY: "top",
223
+ },
224
+ });
225
+
226
+ if (!includeImageClipEffect) {
227
+ return { specs, effects: [] };
228
+ }
229
+
230
+ const clipPathData = generateDielinePath({
231
+ ...common,
232
+ width: cutW,
233
+ height: cutH,
234
+ radius: cutR,
235
+ x: cx,
236
+ y: cy,
237
+ features: cutFeatures,
238
+ });
239
+
240
+ if (!clipPathData) {
241
+ return { specs, effects: [] };
242
+ }
243
+
244
+ return {
245
+ specs,
246
+ effects: [
247
+ {
248
+ type: "clipPath",
249
+ id: ids.clip,
250
+ visibility: clipVisibility,
251
+ targetPassIds: clipTargetPassIds,
252
+ source: {
253
+ id: ids.clipSource,
254
+ type: "path",
255
+ space: "screen",
256
+ data: {
257
+ id: ids.clipSource,
258
+ type: "dieline-effect",
259
+ effect: "clipPath",
260
+ },
261
+ props: {
262
+ pathData: clipPathData,
263
+ fill: "#000000",
264
+ stroke: null,
265
+ originX: "left",
266
+ originY: "top",
267
+ selectable: false,
268
+ evented: false,
269
+ excludeFromExport: true,
270
+ },
271
+ },
272
+ },
273
+ ],
274
+ };
275
+ }