@overtone-art/canvas-editor-core 0.2.8 → 0.3.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.
@@ -106,6 +106,38 @@ async function exportIsolatedPNG(source, objects, options = {}) {
106
106
  canvas.dispose();
107
107
  }
108
108
  }
109
+ async function exportPrintArea(source, area, options = {}) {
110
+ const { multiplier = 1, format = "png", quality = 1 } = options;
111
+ const width = source.getWidth();
112
+ const height = source.getHeight();
113
+ const clip = computePrintAreaClip(
114
+ area,
115
+ multiplier,
116
+ multiplier,
117
+ width * multiplier,
118
+ height * multiplier
119
+ );
120
+ if (clip.width <= 0 || clip.height <= 0) {
121
+ throw new Error("Print area does not overlap the canvas");
122
+ }
123
+ const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
124
+ const canvas = new StaticCanvas(element, { width, height });
125
+ try {
126
+ const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
127
+ if (clones.length) canvas.add(...clones);
128
+ canvas.requestRenderAll();
129
+ const rendered = canvas.toCanvasElement(multiplier);
130
+ const output = rendered.ownerDocument.createElement("canvas");
131
+ output.width = Math.max(1, Math.round(clip.width));
132
+ output.height = Math.max(1, Math.round(clip.height));
133
+ const context = output.getContext("2d");
134
+ if (!context) throw new Error("2D canvas context is unavailable");
135
+ context.drawImage(rendered, -clip.left, -clip.top);
136
+ return await canvasElementToBlob(output, format, quality);
137
+ } finally {
138
+ canvas.dispose();
139
+ }
140
+ }
109
141
  async function exportMockup(canvas, mockup, options = {}) {
110
142
  const { multiplier = 1, format = "png", quality = 1 } = options;
111
143
  const design = canvas.toCanvasElement(multiplier);
@@ -221,8 +253,9 @@ export {
221
253
  computeCoverPlacement,
222
254
  exportPNG,
223
255
  exportIsolatedPNG,
256
+ exportPrintArea,
224
257
  exportMockup,
225
258
  exportSVG,
226
259
  exportDataURL
227
260
  };
228
- //# sourceMappingURL=chunk-ORZZ6MGQ.mjs.map
261
+ //# sourceMappingURL=chunk-MCBRZQ4M.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/displacement.ts","../src/export.ts"],"sourcesContent":["import type { MockupDisplacement, MockupDisplacementChannel } from './types';\n\nconst CHANNEL_INDEX: Record<MockupDisplacementChannel, number> = {\n red: 0,\n green: 1,\n blue: 2,\n alpha: 3,\n};\n\nfunction finiteScale(value: number | undefined, fallback: number, label: string): number {\n const resolved = value ?? fallback;\n if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);\n return resolved;\n}\n\nfunction sample(\n source: Uint8ClampedArray,\n width: number,\n height: number,\n x: number,\n y: number,\n channel: number,\n): number {\n const clampedX = Math.max(0, Math.min(width - 1, x));\n const clampedY = Math.max(0, Math.min(height - 1, y));\n const x0 = Math.floor(clampedX);\n const y0 = Math.floor(clampedY);\n const x1 = Math.min(width - 1, x0 + 1);\n const y1 = Math.min(height - 1, y0 + 1);\n const tx = clampedX - x0;\n const ty = clampedY - y0;\n const top =\n source[(y0 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y0 * width + x1) * 4 + channel] * tx;\n const bottom =\n source[(y1 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y1 * width + x1) * 4 + channel] * tx;\n return top * (1 - ty) + bottom * ty;\n}\n\n/**\n * Warp RGBA pixels with an equally sized channel map. A channel value of 128\n * is neutral; 0 and 255 move by the configured negative/positive maximum.\n */\nexport function displaceRgba(\n source: Uint8ClampedArray,\n map: Uint8ClampedArray,\n width: number,\n height: number,\n options: Omit<MockupDisplacement, 'image'>,\n): Uint8ClampedArray {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {\n throw new Error('Displacement dimensions must be positive integers');\n }\n const expectedLength = width * height * 4;\n if (source.length !== expectedLength || map.length !== expectedLength) {\n throw new Error('Displacement source and map must match the requested dimensions');\n }\n\n const scaleX = finiteScale(options.scaleX, 10, 'Displacement scaleX');\n const scaleY = finiteScale(options.scaleY, 10, 'Displacement scaleY');\n const channelX = CHANNEL_INDEX[options.channelX ?? 'red'];\n const channelY = CHANNEL_INDEX[options.channelY ?? 'green'];\n const output = new Uint8ClampedArray(expectedLength);\n\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = (y * width + x) * 4;\n const sourceX = x + ((map[offset + channelX] - 128) / 127) * scaleX;\n const sourceY = y + ((map[offset + channelY] - 128) / 127) * scaleY;\n for (let channel = 0; channel < 4; channel += 1) {\n output[offset + channel] = Math.round(\n sample(source, width, height, sourceX, sourceY, channel),\n );\n }\n }\n }\n return output;\n}\n","import { StaticCanvas } from 'fabric';\nimport type { Canvas, FabricObject, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\nimport { displaceRgba } from './displacement';\n\nexport interface PngExportOptions {\n multiplier?: number;\n format?: ImageFormat;\n quality?: number;\n}\n\nexport interface CoverPlacement {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport function computePrintAreaClip(\n area: MockupPrintArea,\n scaleX: number,\n scaleY: number,\n targetWidth: number,\n targetHeight: number,\n): MockupPrintArea {\n const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));\n const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));\n const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));\n const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));\n return { left, top, width: right - left, height: bottom - top };\n}\n\n/** Object-fit: cover geometry, exported for deterministic preview/composite tests. */\nexport function computeCoverPlacement(\n sourceWidth: number,\n sourceHeight: number,\n targetWidth: number,\n targetHeight: number,\n): CoverPlacement {\n if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {\n throw new Error('Cover dimensions must be positive');\n }\n const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);\n const width = sourceWidth * scale;\n const height = sourceHeight * scale;\n return {\n left: (targetWidth - width) / 2,\n top: (targetHeight - height) / 2,\n width,\n height,\n };\n}\n\nfunction canvasElementToBlob(\n output: HTMLCanvasElement,\n format: ImageFormat,\n quality: number,\n): Promise<Blob> {\n const mime = format === 'jpeg' ? 'image/jpeg' : `image/${format}`;\n return new Promise<Blob>((resolve, reject) => {\n output.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`))),\n mime,\n quality,\n );\n });\n}\n\nexport async function exportPNG(canvas: Canvas, options: PngExportOptions = {}): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const output = canvas.toCanvasElement(multiplier);\n return canvasElementToBlob(output, format, quality);\n}\n\n/** Render cloned objects without mutating the live editor canvas. */\nexport async function exportIsolatedPNG(\n source: Canvas,\n objects: FabricObject[],\n options: PngExportOptions & {\n width?: number;\n height?: number;\n backgroundColor?: string;\n backgroundImage?: FabricObject | null;\n cloneObjects?: boolean;\n } = {},\n): Promise<Blob> {\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, {\n width: options.width ?? source.getWidth(),\n height: options.height ?? source.getHeight(),\n backgroundColor: options.backgroundColor || undefined,\n });\n try {\n const clones =\n options.cloneObjects === false\n ? objects\n : await Promise.all(objects.map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();\n canvas.requestRenderAll();\n // Awaited, not returned: `finally` would otherwise dispose the canvas while\n // the export is still reading from it.\n return await exportPNG(canvas as unknown as Canvas, options);\n } finally {\n canvas.dispose();\n }\n}\n\n/**\n * Render just the print-area rectangle, on transparency.\n *\n * This is the file a print provider receives: the design alone, cropped to the\n * printable rectangle, with no garment behind it and no canvas background baked\n * in — so it is rendered from cloned objects rather than off the live canvas.\n */\nexport async function exportPrintArea(\n source: Canvas,\n area: MockupPrintArea,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const width = source.getWidth();\n const height = source.getHeight();\n const clip = computePrintAreaClip(\n area,\n multiplier,\n multiplier,\n width * multiplier,\n height * multiplier,\n );\n if (clip.width <= 0 || clip.height <= 0) {\n throw new Error('Print area does not overlap the canvas');\n }\n\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, { width, height });\n try {\n const clones = await Promise.all(source.getObjects().map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n canvas.requestRenderAll();\n const rendered = canvas.toCanvasElement(multiplier);\n const output = rendered.ownerDocument.createElement('canvas');\n output.width = Math.max(1, Math.round(clip.width));\n output.height = Math.max(1, Math.round(clip.height));\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n context.drawImage(rendered, -clip.left, -clip.top);\n // Awaited, not returned: `finally` would dispose the canvas mid-read.\n return await canvasElementToBlob(output, format, quality);\n } finally {\n canvas.dispose();\n }\n}\n\n/** Rasterize the browser mockup preview together with the transparent design. */\nexport async function exportMockup(\n canvas: Canvas,\n mockup: MockupConfig,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const design = canvas.toCanvasElement(multiplier);\n const output = design.ownerDocument.createElement('canvas');\n output.width = design.width;\n output.height = design.height;\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n\n const loadImage = (url: string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const element = new Image();\n element.crossOrigin = 'anonymous';\n element.onload = () => resolve(element);\n element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));\n element.src = url;\n });\n const drawCover = (\n image: HTMLImageElement,\n targetContext: CanvasRenderingContext2D = context,\n ) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n targetContext.drawImage(\n image,\n placement.left,\n placement.top,\n placement.width,\n placement.height,\n );\n };\n\n // Full-size scratch buffers, released explicitly in the finally below: a\n // detached canvas element can hold its backing store well past its last\n // reference, and a 4K mockup allocates three of them per export.\n const scratch: HTMLCanvasElement[] = [design];\n try {\n drawCover(await loadImage(mockup.image));\n let compositedDesign: CanvasImageSource = design;\n if (mockup.displacement) {\n const sourceContext = design.getContext('2d');\n if (!sourceContext) throw new Error('2D design context is unavailable');\n const mapCanvas = design.ownerDocument.createElement('canvas');\n scratch.push(mapCanvas);\n mapCanvas.width = design.width;\n mapCanvas.height = design.height;\n const mapContext = mapCanvas.getContext('2d');\n if (!mapContext) throw new Error('2D displacement-map context is unavailable');\n drawCover(await loadImage(mockup.displacement.image), mapContext);\n\n const warped = design.ownerDocument.createElement('canvas');\n scratch.push(warped);\n warped.width = design.width;\n warped.height = design.height;\n const warpedContext = warped.getContext('2d');\n if (!warpedContext) throw new Error('2D displaced-design context is unavailable');\n let sourcePixels: Uint8ClampedArray;\n let mapPixels: Uint8ClampedArray;\n try {\n sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;\n mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;\n } catch (error) {\n throw new Error('Failed to apply mockup displacement map; verify image CORS access', {\n cause: error,\n });\n }\n const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {\n ...mockup.displacement,\n scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,\n scaleY: (mockup.displacement.scaleY ?? 10) * multiplier,\n });\n const imageData = warpedContext.createImageData(design.width, design.height);\n imageData.data.set(pixels);\n warpedContext.putImageData(imageData, 0, 0);\n compositedDesign = warped;\n }\n context.save();\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n output.width / canvas.getWidth(),\n output.height / canvas.getHeight(),\n output.width,\n output.height,\n );\n context.beginPath();\n context.rect(clip.left, clip.top, clip.width, clip.height);\n context.clip();\n }\n context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));\n context.globalCompositeOperation =\n !mockup.designBlendMode || mockup.designBlendMode === 'normal'\n ? 'source-over'\n : mockup.designBlendMode;\n context.drawImage(compositedDesign, 0, 0);\n context.restore();\n\n if (mockup.overlay) {\n context.save();\n context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));\n context.globalCompositeOperation =\n mockup.overlay.blendMode === 'normal'\n ? 'source-over'\n : (mockup.overlay.blendMode ?? 'multiply');\n drawCover(await loadImage(mockup.overlay.image));\n context.restore();\n }\n return await canvasElementToBlob(output, format, quality);\n } finally {\n for (const element of scratch) {\n element.width = 0;\n element.height = 0;\n }\n }\n}\n\nexport function exportSVG(canvas: Canvas): string {\n return canvas.toSVG();\n}\n\nexport function exportDataURL(canvas: Canvas, format: ImageFormat = 'png', multiplier = 1): string {\n return canvas.toDataURL({ format, multiplier });\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Canvas, FabricObject, ImageFormat } from 'fabric';
2
- import { b as EditorEvents, l as LayerType, k as LayerMeta, L as LayerData, U as SerializedLayer, G as PatternSourceResolver, P as PatternConfig, A as PatternLocks, _ as Unit, z as MockupPrintArea, v as MockupConfig, e as FontDefinition, m as LicenseConfig, o as LicenseStatus, Q as ProjectState, M as MaskBrushOptions, p as MaskPoint, r as MaskRefinementProvider, q as MaskRefinementPrompt, t as MaskRefinementResult, E as EditorConfig, V as ShapePlugin, X as TemplateDefinition, I as ImageAdjustments, c as EditorState, S as SemanticExportOptions, W as SvgExportOptions, K as PrintifyPositioning, N as NormalizedLayerPosition, J as PositioningAdapter, h as ImageProviderResult, i as ImageSearchOptions, j as ImageSearchResult, F as FileAdapter, g as ImageProvider, T as SerializedBackgroundImageOptions, B as BackgroundImageOptions, R as ResizeOptions, a as DpiIssue, C as CanvasSizePreset, w as MockupDisplacement } from './types-D60CfxL9.mjs';
3
- export { D as DEFAULT_PATTERN_CONFIG, d as ExportFormat, f as ImageAttribution, n as LicensePayload, s as MaskRefinementRequest, u as MockupBlendMode, x as MockupDisplacementChannel, y as MockupOverlay, H as PatternState, O as ProjectPage, Y as TemplateParameter, Z as TileMode } from './types-D60CfxL9.mjs';
2
+ import { b as EditorEvents, m as LayerType, k as LayerMeta, L as LayerData, W as SerializedLayer, J as PatternSourceResolver, P as PatternConfig, H as PatternLocks, a0 as TextCurveConfig, r as MaskPresetId, X as ShapeMaskPresetId, a1 as TextureMaskPresetId, a3 as Unit, G as MockupPrintArea, x as MockupConfig, e as FontDefinition, n as LicenseConfig, p as LicenseStatus, S as ProjectState, M as MaskBrushOptions, q as MaskPoint, t as MaskRefinementProvider, s as MaskRefinementPrompt, v as MaskRefinementResult, E as EditorConfig, Y as ShapePlugin, _ as TemplateDefinition, I as ImageAdjustments, c as EditorState, U as SemanticExportOptions, Z as SvgExportOptions, Q as PrintifyPositioning, N as NormalizedLayerPosition, O as PositioningAdapter, h as ImageProviderResult, i as ImageSearchOptions, j as ImageSearchResult, F as FileAdapter, g as ImageProvider, V as SerializedBackgroundImageOptions, B as BackgroundImageOptions, T as ResizeOptions, a as DpiIssue, l as LayerShadowConfig, C as CanvasSizePreset, y as MockupDisplacement } from './types-Dh1unrzT.mjs';
3
+ export { D as DEFAULT_PATTERN_CONFIG, d as ExportFormat, f as ImageAttribution, o as LicensePayload, u as MaskRefinementRequest, w as MockupBlendMode, z as MockupDisplacementChannel, A as MockupOverlay, K as PatternState, R as ProjectPage, $ as TemplateParameter, a2 as TileMode } from './types-Dh1unrzT.mjs';
4
4
 
5
5
  type EventHandler<T> = (data: T) => void;
6
6
  declare class EventEmitter<TEvents extends {} = Record<string, unknown>> {
@@ -47,6 +47,18 @@ declare class LayerManager {
47
47
  setLocked(id: string, locked: boolean): void;
48
48
  setOpacity(id: string, opacity: number): void;
49
49
  setName(id: string, name: string): void;
50
+ /**
51
+ * Patch a layer's host metadata. A key whose value is `undefined` is removed.
52
+ *
53
+ * `meta` is part of `LayerData` and is serialized, so a host that assigns
54
+ * `layer.meta.x` directly gets neither of the two things every other setter
55
+ * here provides: `layers:changed` (so `useLayers()` keeps returning the old
56
+ * meta) and the history checkpoint (so an undo silently reverts the write —
57
+ * the state IS versioned, it was just never committed at the moment it
58
+ * changed). Going through this method is what makes metadata behave like
59
+ * every other layer property.
60
+ */
61
+ setMeta(id: string, patch: LayerMeta): void;
50
62
  clear(): void;
51
63
  count(): number;
52
64
  private emitChanged;
@@ -246,6 +258,107 @@ interface CanvasRenderingProtocol {
246
258
  drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
247
259
  }
248
260
 
261
+ /** Straight text — the state every text layer starts in. */
262
+ declare const DEFAULT_TEXT_CURVE: TextCurveConfig;
263
+ interface CurvePath {
264
+ /** SVG path data the text is laid out along. */
265
+ data: string;
266
+ /** Length of that path, used to centre the run on it. */
267
+ length: number;
268
+ }
269
+ /** Path the given curve traces for a text run of `width` px, or null when straight. */
270
+ declare function buildCurvePathData(config: TextCurveConfig, width: number, fontSize: number): CurvePath | null;
271
+ /**
272
+ * Bends a text layer's baseline along a generated path.
273
+ *
274
+ * The curve lives on the fabric object as a real text path, so it survives
275
+ * export and serialization with no extra machinery. The parameters that
276
+ * produced it are kept in `layer.meta.curve` so the UI can show them and so the
277
+ * path can be rebuilt when the text, font or size changes.
278
+ */
279
+ declare class TextCurveManager {
280
+ private canvas;
281
+ private layers;
282
+ private history;
283
+ private events;
284
+ constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager, events: EventEmitter<EditorEvents>);
285
+ /** Curve parameters for a layer, or null when it is not curved text. */
286
+ get(layerId: string): TextCurveConfig | null;
287
+ isCurved(layerId: string): boolean;
288
+ /** Apply (or update) the curve on a text layer. Zeroed config clears it. */
289
+ apply(layerId: string, config: Partial<TextCurveConfig>, save?: boolean): boolean;
290
+ /** Remove the curve, restoring the authored text box width. */
291
+ clear(layerId: string, save?: boolean): boolean;
292
+ /**
293
+ * Rebuild the path from the stored parameters. Text content, font family and
294
+ * font size all change the run's width, and the path is sized to that width —
295
+ * without this the curve keeps the geometry of the text it was created from.
296
+ */
297
+ refresh(layerId: string, save?: boolean): boolean;
298
+ /** Rebuild every curved layer — used after a state restore. */
299
+ refreshAll(): void;
300
+ private detach;
301
+ }
302
+
303
+ declare function isMaskPresetId(value: unknown): value is MaskPresetId;
304
+ /**
305
+ * Clips a layer to a preset silhouette or soft texture.
306
+ *
307
+ * The preset is installed as the fabric object's `clipPath`, so it renders,
308
+ * exports and serializes through the same path fabric already uses. The chosen
309
+ * id is kept in `layer.meta.maskPreset` so the UI can show the active preset
310
+ * and so the clip can be rebuilt when the layer is resized.
311
+ *
312
+ * A pattern strips and restores the layer's clip while it is enabled (see
313
+ * `PatternManager`), so a mask preset is suspended for the duration of one.
314
+ */
315
+ declare class MaskPresetManager {
316
+ private canvas;
317
+ private layers;
318
+ private history;
319
+ private events;
320
+ constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager, events: EventEmitter<EditorEvents>);
321
+ get(layerId: string): MaskPresetId | null;
322
+ /** Clip the layer to `id`. Passing null (or an unknown id) clears the clip. */
323
+ apply(layerId: string, id: MaskPresetId | null, save?: boolean): boolean;
324
+ clear(layerId: string, save?: boolean): boolean;
325
+ /**
326
+ * Re-fit the clip to the layer's current size. The clip is built for the
327
+ * object's dimensions at the time it was applied; editing text or replacing an
328
+ * image changes them, and a stale clip would crop the wrong region.
329
+ */
330
+ refresh(layerId: string, save?: boolean): boolean;
331
+ /** Re-fit every masked layer — used after a state restore. */
332
+ refreshAll(): void;
333
+ private buildClip;
334
+ }
335
+
336
+ /** Silhouette masks, authored on a 0–100 square so they scale to any layer. */
337
+ declare const SHAPE_MASK_IDS: readonly ["circle", "square", "triangle", "star", "heart", "octagram", "arch", "zigzag"];
338
+ type ShapeMaskId = ShapeMaskPresetId;
339
+ declare function isShapeMaskId(value: unknown): value is ShapeMaskId;
340
+ /** SVG path data for a shape mask, on a 100×100 box. */
341
+ declare function shapeMaskPathData(id: ShapeMaskId): string;
342
+ /** Nominal authoring box every shape mask path is drawn in. */
343
+ declare const SHAPE_MASK_BOX = 100;
344
+
345
+ /**
346
+ * Procedural alpha textures used as soft layer masks.
347
+ *
348
+ * Each renders white-on-transparent at a fixed resolution; the opaque parts are
349
+ * what survives masking. They are deterministic (seeded PRNG, no `Math.random`)
350
+ * so a design renders identically on every load and in every export.
351
+ */
352
+ declare const TEXTURE_MASK_IDS: readonly ["vignette", "halftone", "spray", "grunge", "torn", "band"];
353
+ type TextureMaskId = TextureMaskPresetId;
354
+ declare function isTextureMaskId(value: unknown): value is TextureMaskId;
355
+ /** Square resolution every texture is rendered at before being scaled to fit. */
356
+ declare const TEXTURE_MASK_SIZE = 320;
357
+ /** Render (and memoize) a texture mask. Browser-only: needs a 2D canvas. */
358
+ declare function renderTextureMask(id: TextureMaskId): HTMLCanvasElement;
359
+ /** Drop memoized textures — used by tests and by long-lived editors on dispose. */
360
+ declare function clearTextureMaskCache(): void;
361
+
249
362
  declare class UnitConverter {
250
363
  private unit;
251
364
  private dpi;
@@ -273,6 +386,14 @@ declare function computePrintAreaClip(area: MockupPrintArea, scaleX: number, sca
273
386
  /** Object-fit: cover geometry, exported for deterministic preview/composite tests. */
274
387
  declare function computeCoverPlacement(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): CoverPlacement;
275
388
  declare function exportPNG(canvas: Canvas, options?: PngExportOptions): Promise<Blob>;
389
+ /**
390
+ * Render just the print-area rectangle, on transparency.
391
+ *
392
+ * This is the file a print provider receives: the design alone, cropped to the
393
+ * printable rectangle, with no garment behind it and no canvas background baked
394
+ * in — so it is rendered from cloned objects rather than off the live canvas.
395
+ */
396
+ declare function exportPrintArea(source: Canvas, area: MockupPrintArea, options?: PngExportOptions): Promise<Blob>;
276
397
  /** Rasterize the browser mockup preview together with the transparent design. */
277
398
  declare function exportMockup(canvas: Canvas, mockup: MockupConfig, options?: PngExportOptions): Promise<Blob>;
278
399
  declare function exportSVG(canvas: Canvas): string;
@@ -390,6 +511,8 @@ declare class CanvasEditor {
390
511
  readonly snapping: SnapManager;
391
512
  readonly crop: CropController;
392
513
  readonly patterns: PatternManager;
514
+ readonly curves: TextCurveManager;
515
+ readonly maskPresets: MaskPresetManager;
393
516
  readonly fonts: FontRegistry;
394
517
  readonly licensing: LicenseManager;
395
518
  readonly pages: ProjectManager;
@@ -433,6 +556,12 @@ declare class CanvasEditor {
433
556
  toSVG(): string;
434
557
  toSVGAsync(options?: SvgExportOptions): Promise<string>;
435
558
  toDataURL(format?: ImageFormat, multiplier?: number): string;
559
+ /**
560
+ * Export the print file: the design cropped to the mockup's print area, on
561
+ * transparency. Without a print area this is the whole canvas, still
562
+ * transparent — a print file never carries the design background.
563
+ */
564
+ toPrintFile(options?: PngExportOptions): Promise<Blob>;
436
565
  /** Export the current product-preview composite. Advanced warping is host-defined. */
437
566
  toMockupImage(options?: PngExportOptions): Promise<Blob>;
438
567
  /**
@@ -484,13 +613,64 @@ declare class CanvasEditor {
484
613
  zoomToSelection(padding?: number): void;
485
614
  applyPattern(layerId: string, config: PatternConfig): Promise<void>;
486
615
  clearPattern(layerId: string): Promise<void>;
487
- setMockup(mockup: MockupConfig | null): void;
616
+ applyTextCurve(layerId: string, config: Partial<TextCurveConfig>): boolean;
617
+ clearTextCurve(layerId: string): boolean;
618
+ getTextCurve(layerId: string): TextCurveConfig | null;
619
+ applyMaskPreset(layerId: string, id: MaskPresetId | null): boolean;
620
+ clearMaskPreset(layerId: string): boolean;
621
+ getMaskPreset(layerId: string): MaskPresetId | null;
622
+ /**
623
+ * Constrain a layer to its current proportions. Persisted on the layer so a
624
+ * reopened design still resizes the way it was set up to.
625
+ */
626
+ setLayerAspectLock(layerId: string, locked: boolean): boolean;
627
+ getLayerAspectLock(layerId: string): boolean;
628
+ /** Re-apply every stored aspect lock — control visibility is not serialized. */
629
+ restoreAspectLocks(): void;
630
+ /** Drop scale, rotation, skew and flips; the layer stays where it is. */
631
+ resetLayerTransform(layerId: string): boolean;
632
+ setLayerShadow(layerId: string, config: Partial<LayerShadowConfig>): boolean;
633
+ getLayerShadow(layerId: string): LayerShadowConfig | null;
634
+ /**
635
+ * Show (or clear) the product preview. Pass `history: false` for preview-only
636
+ * changes such as swapping a colourway — those are not design edits and
637
+ * should not fill the undo stack.
638
+ */
639
+ setMockup(mockup: MockupConfig | null, options?: {
640
+ history?: boolean;
641
+ }): void;
488
642
  clearMockup(): void;
489
643
  getMockup(): MockupConfig | null;
490
644
  dispose(): void;
491
645
  private setupCanvasEvents;
492
646
  }
493
647
 
648
+ declare const DEFAULT_LAYER_SHADOW: LayerShadowConfig;
649
+ /**
650
+ * Read a layer's drop shadow back off its fabric object.
651
+ *
652
+ * Fabric serializes `shadow` with the object, so the object — not layer meta —
653
+ * is the source of truth; nothing extra has to round-trip through the editor
654
+ * state for a shadow to survive save/load.
655
+ */
656
+ declare function readLayerShadow(object: FabricObject): LayerShadowConfig;
657
+ /** Install (or remove) a drop shadow on a fabric object. */
658
+ declare function applyLayerShadow(object: FabricObject, config: Partial<LayerShadowConfig>): void;
659
+
660
+ /**
661
+ * Constrain (or release) a layer's proportions.
662
+ *
663
+ * Fabric already scales uniformly from the corners (`canvas.uniformScaling`),
664
+ * so the lock is enforced by taking away the single-axis handles — every
665
+ * remaining control keeps the ratio.
666
+ */
667
+ declare function applyAspectLock(object: FabricObject, locked: boolean): void;
668
+ /**
669
+ * Undo scaling, rotation, skew and flips, leaving the layer where it sits.
670
+ * Position is deliberately kept: this is "reset the shape", not "move it back".
671
+ */
672
+ declare function resetTransform(object: FabricObject): void;
673
+
494
674
  declare const generateId: () => string;
495
675
 
496
676
  declare function clamp(v: number, min: number, max: number): number;
@@ -590,4 +770,4 @@ declare class AnnotationOverlay {
590
770
  */
591
771
  declare function displaceRgba(source: Uint8ClampedArray, map: Uint8ClampedArray, width: number, height: number, options: Omit<MockupDisplacement, 'image'>): Uint8ClampedArray;
592
772
 
593
- export { AnnotationOverlay, type AnnotationPrimitive, BackgroundImageOptions, CANVAS_SIZE_PRESETS, CanvasEditor, CanvasSizePreset, type CoverPlacement, CropController, DpiIssue, EditorConfig, EditorEvents, EditorState, EventEmitter, FileAdapter, FontDefinition, FontRegistry, HistoryManager, ImageAdjustments, ImageProvider, ImageProviderResult, ImageSearchOptions, ImageSearchResult, Layer, LayerData, LayerManager, LayerMeta, LayerType, LicenseConfig, LicenseManager, LicenseStatus, MaskBrushOptions, MaskController, type MaskPerformanceSample, MaskPoint, MaskRefinementError, type MaskRefinementErrorCode, MaskRefinementPrompt, MaskRefinementProvider, MaskRefinementResult, MockupConfig, MockupDisplacement, MockupPrintArea, NormalizedLayerPosition, PatternConfig, PatternLocks, PatternManager, PatternSourceResolver, type PngExportOptions, PositioningAdapter, PrintifyPositioning, ProjectManager, ProjectState, ResizeOptions, SemanticExportOptions, SerializedBackgroundImageOptions, SerializedLayer, ShapePlugin, SnapManager, SvgExportOptions, TemplateDefinition, type TilePlacement, Unit, UnitConverter, type ViewportTransform, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeCoverPlacement, computePrintAreaClip, computeTilePositions, deserializeEditor, displaceRgba, drawTiles, escapeXml, exportDataURL, exportMockup, exportPNG, exportSVG, generateId, isCssColor, loadPatternImage, restoreLocks, round2, sanitizeSvg, serializeEditor };
773
+ export { AnnotationOverlay, type AnnotationPrimitive, BackgroundImageOptions, CANVAS_SIZE_PRESETS, CanvasEditor, CanvasSizePreset, type CoverPlacement, CropController, DEFAULT_LAYER_SHADOW, DEFAULT_TEXT_CURVE, DpiIssue, EditorConfig, EditorEvents, EditorState, EventEmitter, FileAdapter, FontDefinition, FontRegistry, HistoryManager, ImageAdjustments, ImageProvider, ImageProviderResult, ImageSearchOptions, ImageSearchResult, Layer, LayerData, LayerManager, LayerMeta, LayerShadowConfig, LayerType, LicenseConfig, LicenseManager, LicenseStatus, MaskBrushOptions, MaskController, type MaskPerformanceSample, MaskPoint, MaskPresetId, MaskPresetManager, MaskRefinementError, type MaskRefinementErrorCode, MaskRefinementPrompt, MaskRefinementProvider, MaskRefinementResult, MockupConfig, MockupDisplacement, MockupPrintArea, NormalizedLayerPosition, PatternConfig, PatternLocks, PatternManager, PatternSourceResolver, type PngExportOptions, PositioningAdapter, PrintifyPositioning, ProjectManager, ProjectState, ResizeOptions, SHAPE_MASK_BOX, SHAPE_MASK_IDS, SemanticExportOptions, SerializedBackgroundImageOptions, SerializedLayer, ShapeMaskPresetId, ShapePlugin, SnapManager, SvgExportOptions, TEXTURE_MASK_IDS, TEXTURE_MASK_SIZE, TemplateDefinition, TextCurveConfig, TextCurveManager, TextureMaskPresetId, type TilePlacement, Unit, UnitConverter, type ViewportTransform, applyAspectLock, applyLayerShadow, applyPatternLocks, buildCurvePathData, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, clearTextureMaskCache, computeCoverPlacement, computePrintAreaClip, computeTilePositions, deserializeEditor, displaceRgba, drawTiles, escapeXml, exportDataURL, exportMockup, exportPNG, exportPrintArea, exportSVG, generateId, isCssColor, isMaskPresetId, isShapeMaskId, isTextureMaskId, loadPatternImage, readLayerShadow, renderTextureMask, resetTransform, restoreLocks, round2, sanitizeSvg, serializeEditor, shapeMaskPathData };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Canvas, FabricObject, ImageFormat } from 'fabric';
2
- import { b as EditorEvents, l as LayerType, k as LayerMeta, L as LayerData, U as SerializedLayer, G as PatternSourceResolver, P as PatternConfig, A as PatternLocks, _ as Unit, z as MockupPrintArea, v as MockupConfig, e as FontDefinition, m as LicenseConfig, o as LicenseStatus, Q as ProjectState, M as MaskBrushOptions, p as MaskPoint, r as MaskRefinementProvider, q as MaskRefinementPrompt, t as MaskRefinementResult, E as EditorConfig, V as ShapePlugin, X as TemplateDefinition, I as ImageAdjustments, c as EditorState, S as SemanticExportOptions, W as SvgExportOptions, K as PrintifyPositioning, N as NormalizedLayerPosition, J as PositioningAdapter, h as ImageProviderResult, i as ImageSearchOptions, j as ImageSearchResult, F as FileAdapter, g as ImageProvider, T as SerializedBackgroundImageOptions, B as BackgroundImageOptions, R as ResizeOptions, a as DpiIssue, C as CanvasSizePreset, w as MockupDisplacement } from './types-D60CfxL9.js';
3
- export { D as DEFAULT_PATTERN_CONFIG, d as ExportFormat, f as ImageAttribution, n as LicensePayload, s as MaskRefinementRequest, u as MockupBlendMode, x as MockupDisplacementChannel, y as MockupOverlay, H as PatternState, O as ProjectPage, Y as TemplateParameter, Z as TileMode } from './types-D60CfxL9.js';
2
+ import { b as EditorEvents, m as LayerType, k as LayerMeta, L as LayerData, W as SerializedLayer, J as PatternSourceResolver, P as PatternConfig, H as PatternLocks, a0 as TextCurveConfig, r as MaskPresetId, X as ShapeMaskPresetId, a1 as TextureMaskPresetId, a3 as Unit, G as MockupPrintArea, x as MockupConfig, e as FontDefinition, n as LicenseConfig, p as LicenseStatus, S as ProjectState, M as MaskBrushOptions, q as MaskPoint, t as MaskRefinementProvider, s as MaskRefinementPrompt, v as MaskRefinementResult, E as EditorConfig, Y as ShapePlugin, _ as TemplateDefinition, I as ImageAdjustments, c as EditorState, U as SemanticExportOptions, Z as SvgExportOptions, Q as PrintifyPositioning, N as NormalizedLayerPosition, O as PositioningAdapter, h as ImageProviderResult, i as ImageSearchOptions, j as ImageSearchResult, F as FileAdapter, g as ImageProvider, V as SerializedBackgroundImageOptions, B as BackgroundImageOptions, T as ResizeOptions, a as DpiIssue, l as LayerShadowConfig, C as CanvasSizePreset, y as MockupDisplacement } from './types-Dh1unrzT.js';
3
+ export { D as DEFAULT_PATTERN_CONFIG, d as ExportFormat, f as ImageAttribution, o as LicensePayload, u as MaskRefinementRequest, w as MockupBlendMode, z as MockupDisplacementChannel, A as MockupOverlay, K as PatternState, R as ProjectPage, $ as TemplateParameter, a2 as TileMode } from './types-Dh1unrzT.js';
4
4
 
5
5
  type EventHandler<T> = (data: T) => void;
6
6
  declare class EventEmitter<TEvents extends {} = Record<string, unknown>> {
@@ -47,6 +47,18 @@ declare class LayerManager {
47
47
  setLocked(id: string, locked: boolean): void;
48
48
  setOpacity(id: string, opacity: number): void;
49
49
  setName(id: string, name: string): void;
50
+ /**
51
+ * Patch a layer's host metadata. A key whose value is `undefined` is removed.
52
+ *
53
+ * `meta` is part of `LayerData` and is serialized, so a host that assigns
54
+ * `layer.meta.x` directly gets neither of the two things every other setter
55
+ * here provides: `layers:changed` (so `useLayers()` keeps returning the old
56
+ * meta) and the history checkpoint (so an undo silently reverts the write —
57
+ * the state IS versioned, it was just never committed at the moment it
58
+ * changed). Going through this method is what makes metadata behave like
59
+ * every other layer property.
60
+ */
61
+ setMeta(id: string, patch: LayerMeta): void;
50
62
  clear(): void;
51
63
  count(): number;
52
64
  private emitChanged;
@@ -246,6 +258,107 @@ interface CanvasRenderingProtocol {
246
258
  drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
247
259
  }
248
260
 
261
+ /** Straight text — the state every text layer starts in. */
262
+ declare const DEFAULT_TEXT_CURVE: TextCurveConfig;
263
+ interface CurvePath {
264
+ /** SVG path data the text is laid out along. */
265
+ data: string;
266
+ /** Length of that path, used to centre the run on it. */
267
+ length: number;
268
+ }
269
+ /** Path the given curve traces for a text run of `width` px, or null when straight. */
270
+ declare function buildCurvePathData(config: TextCurveConfig, width: number, fontSize: number): CurvePath | null;
271
+ /**
272
+ * Bends a text layer's baseline along a generated path.
273
+ *
274
+ * The curve lives on the fabric object as a real text path, so it survives
275
+ * export and serialization with no extra machinery. The parameters that
276
+ * produced it are kept in `layer.meta.curve` so the UI can show them and so the
277
+ * path can be rebuilt when the text, font or size changes.
278
+ */
279
+ declare class TextCurveManager {
280
+ private canvas;
281
+ private layers;
282
+ private history;
283
+ private events;
284
+ constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager, events: EventEmitter<EditorEvents>);
285
+ /** Curve parameters for a layer, or null when it is not curved text. */
286
+ get(layerId: string): TextCurveConfig | null;
287
+ isCurved(layerId: string): boolean;
288
+ /** Apply (or update) the curve on a text layer. Zeroed config clears it. */
289
+ apply(layerId: string, config: Partial<TextCurveConfig>, save?: boolean): boolean;
290
+ /** Remove the curve, restoring the authored text box width. */
291
+ clear(layerId: string, save?: boolean): boolean;
292
+ /**
293
+ * Rebuild the path from the stored parameters. Text content, font family and
294
+ * font size all change the run's width, and the path is sized to that width —
295
+ * without this the curve keeps the geometry of the text it was created from.
296
+ */
297
+ refresh(layerId: string, save?: boolean): boolean;
298
+ /** Rebuild every curved layer — used after a state restore. */
299
+ refreshAll(): void;
300
+ private detach;
301
+ }
302
+
303
+ declare function isMaskPresetId(value: unknown): value is MaskPresetId;
304
+ /**
305
+ * Clips a layer to a preset silhouette or soft texture.
306
+ *
307
+ * The preset is installed as the fabric object's `clipPath`, so it renders,
308
+ * exports and serializes through the same path fabric already uses. The chosen
309
+ * id is kept in `layer.meta.maskPreset` so the UI can show the active preset
310
+ * and so the clip can be rebuilt when the layer is resized.
311
+ *
312
+ * A pattern strips and restores the layer's clip while it is enabled (see
313
+ * `PatternManager`), so a mask preset is suspended for the duration of one.
314
+ */
315
+ declare class MaskPresetManager {
316
+ private canvas;
317
+ private layers;
318
+ private history;
319
+ private events;
320
+ constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager, events: EventEmitter<EditorEvents>);
321
+ get(layerId: string): MaskPresetId | null;
322
+ /** Clip the layer to `id`. Passing null (or an unknown id) clears the clip. */
323
+ apply(layerId: string, id: MaskPresetId | null, save?: boolean): boolean;
324
+ clear(layerId: string, save?: boolean): boolean;
325
+ /**
326
+ * Re-fit the clip to the layer's current size. The clip is built for the
327
+ * object's dimensions at the time it was applied; editing text or replacing an
328
+ * image changes them, and a stale clip would crop the wrong region.
329
+ */
330
+ refresh(layerId: string, save?: boolean): boolean;
331
+ /** Re-fit every masked layer — used after a state restore. */
332
+ refreshAll(): void;
333
+ private buildClip;
334
+ }
335
+
336
+ /** Silhouette masks, authored on a 0–100 square so they scale to any layer. */
337
+ declare const SHAPE_MASK_IDS: readonly ["circle", "square", "triangle", "star", "heart", "octagram", "arch", "zigzag"];
338
+ type ShapeMaskId = ShapeMaskPresetId;
339
+ declare function isShapeMaskId(value: unknown): value is ShapeMaskId;
340
+ /** SVG path data for a shape mask, on a 100×100 box. */
341
+ declare function shapeMaskPathData(id: ShapeMaskId): string;
342
+ /** Nominal authoring box every shape mask path is drawn in. */
343
+ declare const SHAPE_MASK_BOX = 100;
344
+
345
+ /**
346
+ * Procedural alpha textures used as soft layer masks.
347
+ *
348
+ * Each renders white-on-transparent at a fixed resolution; the opaque parts are
349
+ * what survives masking. They are deterministic (seeded PRNG, no `Math.random`)
350
+ * so a design renders identically on every load and in every export.
351
+ */
352
+ declare const TEXTURE_MASK_IDS: readonly ["vignette", "halftone", "spray", "grunge", "torn", "band"];
353
+ type TextureMaskId = TextureMaskPresetId;
354
+ declare function isTextureMaskId(value: unknown): value is TextureMaskId;
355
+ /** Square resolution every texture is rendered at before being scaled to fit. */
356
+ declare const TEXTURE_MASK_SIZE = 320;
357
+ /** Render (and memoize) a texture mask. Browser-only: needs a 2D canvas. */
358
+ declare function renderTextureMask(id: TextureMaskId): HTMLCanvasElement;
359
+ /** Drop memoized textures — used by tests and by long-lived editors on dispose. */
360
+ declare function clearTextureMaskCache(): void;
361
+
249
362
  declare class UnitConverter {
250
363
  private unit;
251
364
  private dpi;
@@ -273,6 +386,14 @@ declare function computePrintAreaClip(area: MockupPrintArea, scaleX: number, sca
273
386
  /** Object-fit: cover geometry, exported for deterministic preview/composite tests. */
274
387
  declare function computeCoverPlacement(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): CoverPlacement;
275
388
  declare function exportPNG(canvas: Canvas, options?: PngExportOptions): Promise<Blob>;
389
+ /**
390
+ * Render just the print-area rectangle, on transparency.
391
+ *
392
+ * This is the file a print provider receives: the design alone, cropped to the
393
+ * printable rectangle, with no garment behind it and no canvas background baked
394
+ * in — so it is rendered from cloned objects rather than off the live canvas.
395
+ */
396
+ declare function exportPrintArea(source: Canvas, area: MockupPrintArea, options?: PngExportOptions): Promise<Blob>;
276
397
  /** Rasterize the browser mockup preview together with the transparent design. */
277
398
  declare function exportMockup(canvas: Canvas, mockup: MockupConfig, options?: PngExportOptions): Promise<Blob>;
278
399
  declare function exportSVG(canvas: Canvas): string;
@@ -390,6 +511,8 @@ declare class CanvasEditor {
390
511
  readonly snapping: SnapManager;
391
512
  readonly crop: CropController;
392
513
  readonly patterns: PatternManager;
514
+ readonly curves: TextCurveManager;
515
+ readonly maskPresets: MaskPresetManager;
393
516
  readonly fonts: FontRegistry;
394
517
  readonly licensing: LicenseManager;
395
518
  readonly pages: ProjectManager;
@@ -433,6 +556,12 @@ declare class CanvasEditor {
433
556
  toSVG(): string;
434
557
  toSVGAsync(options?: SvgExportOptions): Promise<string>;
435
558
  toDataURL(format?: ImageFormat, multiplier?: number): string;
559
+ /**
560
+ * Export the print file: the design cropped to the mockup's print area, on
561
+ * transparency. Without a print area this is the whole canvas, still
562
+ * transparent — a print file never carries the design background.
563
+ */
564
+ toPrintFile(options?: PngExportOptions): Promise<Blob>;
436
565
  /** Export the current product-preview composite. Advanced warping is host-defined. */
437
566
  toMockupImage(options?: PngExportOptions): Promise<Blob>;
438
567
  /**
@@ -484,13 +613,64 @@ declare class CanvasEditor {
484
613
  zoomToSelection(padding?: number): void;
485
614
  applyPattern(layerId: string, config: PatternConfig): Promise<void>;
486
615
  clearPattern(layerId: string): Promise<void>;
487
- setMockup(mockup: MockupConfig | null): void;
616
+ applyTextCurve(layerId: string, config: Partial<TextCurveConfig>): boolean;
617
+ clearTextCurve(layerId: string): boolean;
618
+ getTextCurve(layerId: string): TextCurveConfig | null;
619
+ applyMaskPreset(layerId: string, id: MaskPresetId | null): boolean;
620
+ clearMaskPreset(layerId: string): boolean;
621
+ getMaskPreset(layerId: string): MaskPresetId | null;
622
+ /**
623
+ * Constrain a layer to its current proportions. Persisted on the layer so a
624
+ * reopened design still resizes the way it was set up to.
625
+ */
626
+ setLayerAspectLock(layerId: string, locked: boolean): boolean;
627
+ getLayerAspectLock(layerId: string): boolean;
628
+ /** Re-apply every stored aspect lock — control visibility is not serialized. */
629
+ restoreAspectLocks(): void;
630
+ /** Drop scale, rotation, skew and flips; the layer stays where it is. */
631
+ resetLayerTransform(layerId: string): boolean;
632
+ setLayerShadow(layerId: string, config: Partial<LayerShadowConfig>): boolean;
633
+ getLayerShadow(layerId: string): LayerShadowConfig | null;
634
+ /**
635
+ * Show (or clear) the product preview. Pass `history: false` for preview-only
636
+ * changes such as swapping a colourway — those are not design edits and
637
+ * should not fill the undo stack.
638
+ */
639
+ setMockup(mockup: MockupConfig | null, options?: {
640
+ history?: boolean;
641
+ }): void;
488
642
  clearMockup(): void;
489
643
  getMockup(): MockupConfig | null;
490
644
  dispose(): void;
491
645
  private setupCanvasEvents;
492
646
  }
493
647
 
648
+ declare const DEFAULT_LAYER_SHADOW: LayerShadowConfig;
649
+ /**
650
+ * Read a layer's drop shadow back off its fabric object.
651
+ *
652
+ * Fabric serializes `shadow` with the object, so the object — not layer meta —
653
+ * is the source of truth; nothing extra has to round-trip through the editor
654
+ * state for a shadow to survive save/load.
655
+ */
656
+ declare function readLayerShadow(object: FabricObject): LayerShadowConfig;
657
+ /** Install (or remove) a drop shadow on a fabric object. */
658
+ declare function applyLayerShadow(object: FabricObject, config: Partial<LayerShadowConfig>): void;
659
+
660
+ /**
661
+ * Constrain (or release) a layer's proportions.
662
+ *
663
+ * Fabric already scales uniformly from the corners (`canvas.uniformScaling`),
664
+ * so the lock is enforced by taking away the single-axis handles — every
665
+ * remaining control keeps the ratio.
666
+ */
667
+ declare function applyAspectLock(object: FabricObject, locked: boolean): void;
668
+ /**
669
+ * Undo scaling, rotation, skew and flips, leaving the layer where it sits.
670
+ * Position is deliberately kept: this is "reset the shape", not "move it back".
671
+ */
672
+ declare function resetTransform(object: FabricObject): void;
673
+
494
674
  declare const generateId: () => string;
495
675
 
496
676
  declare function clamp(v: number, min: number, max: number): number;
@@ -590,4 +770,4 @@ declare class AnnotationOverlay {
590
770
  */
591
771
  declare function displaceRgba(source: Uint8ClampedArray, map: Uint8ClampedArray, width: number, height: number, options: Omit<MockupDisplacement, 'image'>): Uint8ClampedArray;
592
772
 
593
- export { AnnotationOverlay, type AnnotationPrimitive, BackgroundImageOptions, CANVAS_SIZE_PRESETS, CanvasEditor, CanvasSizePreset, type CoverPlacement, CropController, DpiIssue, EditorConfig, EditorEvents, EditorState, EventEmitter, FileAdapter, FontDefinition, FontRegistry, HistoryManager, ImageAdjustments, ImageProvider, ImageProviderResult, ImageSearchOptions, ImageSearchResult, Layer, LayerData, LayerManager, LayerMeta, LayerType, LicenseConfig, LicenseManager, LicenseStatus, MaskBrushOptions, MaskController, type MaskPerformanceSample, MaskPoint, MaskRefinementError, type MaskRefinementErrorCode, MaskRefinementPrompt, MaskRefinementProvider, MaskRefinementResult, MockupConfig, MockupDisplacement, MockupPrintArea, NormalizedLayerPosition, PatternConfig, PatternLocks, PatternManager, PatternSourceResolver, type PngExportOptions, PositioningAdapter, PrintifyPositioning, ProjectManager, ProjectState, ResizeOptions, SemanticExportOptions, SerializedBackgroundImageOptions, SerializedLayer, ShapePlugin, SnapManager, SvgExportOptions, TemplateDefinition, type TilePlacement, Unit, UnitConverter, type ViewportTransform, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeCoverPlacement, computePrintAreaClip, computeTilePositions, deserializeEditor, displaceRgba, drawTiles, escapeXml, exportDataURL, exportMockup, exportPNG, exportSVG, generateId, isCssColor, loadPatternImage, restoreLocks, round2, sanitizeSvg, serializeEditor };
773
+ export { AnnotationOverlay, type AnnotationPrimitive, BackgroundImageOptions, CANVAS_SIZE_PRESETS, CanvasEditor, CanvasSizePreset, type CoverPlacement, CropController, DEFAULT_LAYER_SHADOW, DEFAULT_TEXT_CURVE, DpiIssue, EditorConfig, EditorEvents, EditorState, EventEmitter, FileAdapter, FontDefinition, FontRegistry, HistoryManager, ImageAdjustments, ImageProvider, ImageProviderResult, ImageSearchOptions, ImageSearchResult, Layer, LayerData, LayerManager, LayerMeta, LayerShadowConfig, LayerType, LicenseConfig, LicenseManager, LicenseStatus, MaskBrushOptions, MaskController, type MaskPerformanceSample, MaskPoint, MaskPresetId, MaskPresetManager, MaskRefinementError, type MaskRefinementErrorCode, MaskRefinementPrompt, MaskRefinementProvider, MaskRefinementResult, MockupConfig, MockupDisplacement, MockupPrintArea, NormalizedLayerPosition, PatternConfig, PatternLocks, PatternManager, PatternSourceResolver, type PngExportOptions, PositioningAdapter, PrintifyPositioning, ProjectManager, ProjectState, ResizeOptions, SHAPE_MASK_BOX, SHAPE_MASK_IDS, SemanticExportOptions, SerializedBackgroundImageOptions, SerializedLayer, ShapeMaskPresetId, ShapePlugin, SnapManager, SvgExportOptions, TEXTURE_MASK_IDS, TEXTURE_MASK_SIZE, TemplateDefinition, TextCurveConfig, TextCurveManager, TextureMaskPresetId, type TilePlacement, Unit, UnitConverter, type ViewportTransform, applyAspectLock, applyLayerShadow, applyPatternLocks, buildCurvePathData, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, clearTextureMaskCache, computeCoverPlacement, computePrintAreaClip, computeTilePositions, deserializeEditor, displaceRgba, drawTiles, escapeXml, exportDataURL, exportMockup, exportPNG, exportPrintArea, exportSVG, generateId, isCssColor, isMaskPresetId, isShapeMaskId, isTextureMaskId, loadPatternImage, readLayerShadow, renderTextureMask, resetTransform, restoreLocks, round2, sanitizeSvg, serializeEditor, shapeMaskPathData };