fabricjs-document-engine 1.1.0 → 1.3.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.
Files changed (146) hide show
  1. package/README.md +70 -1
  2. package/README.zh-CN.md +71 -1
  3. package/dist/assets/asset-pipeline.cjs +10 -8
  4. package/dist/assets/asset-pipeline.d.cts +1 -1
  5. package/dist/assets/asset-pipeline.d.ts +1 -1
  6. package/dist/assets/asset-pipeline.js +10 -8
  7. package/dist/assets/image-check.cjs +4 -3
  8. package/dist/assets/image-check.d.cts +1 -1
  9. package/dist/assets/image-check.d.ts +1 -1
  10. package/dist/assets/image-check.js +4 -3
  11. package/dist/commands/clipboard.cjs +5 -0
  12. package/dist/commands/clipboard.js +5 -0
  13. package/dist/commands/text-edit.cjs +223 -0
  14. package/dist/commands/text-edit.d.cts +23 -0
  15. package/dist/commands/text-edit.d.ts +23 -0
  16. package/dist/commands/text-edit.js +218 -0
  17. package/dist/document/document-format.d.cts +15 -0
  18. package/dist/document/document-format.d.ts +15 -0
  19. package/dist/document/validate-document.cjs +8 -0
  20. package/dist/document/validate-document.js +8 -0
  21. package/dist/engine/create-document-engine.cjs +115 -21
  22. package/dist/engine/create-document-engine.d.cts +24 -6
  23. package/dist/engine/create-document-engine.d.ts +24 -6
  24. package/dist/engine/create-document-engine.js +116 -22
  25. package/dist/engine/errors.d.cts +1 -1
  26. package/dist/engine/errors.d.ts +1 -1
  27. package/dist/export/export-options.cjs +3 -0
  28. package/dist/export/export-options.d.cts +6 -0
  29. package/dist/export/export-options.d.ts +6 -0
  30. package/dist/export/export-options.js +3 -0
  31. package/dist/export/preflight-export.d.cts +1 -1
  32. package/dist/export/preflight-export.d.ts +1 -1
  33. package/dist/export/render-export.cjs +42 -4
  34. package/dist/export/render-export.js +43 -5
  35. package/dist/export/svg/clip-masks.cjs +39 -0
  36. package/dist/export/svg/clip-masks.js +37 -0
  37. package/dist/export/svg/embed-assets.cjs +1 -0
  38. package/dist/export/svg/embed-assets.js +1 -0
  39. package/dist/export/svg/raster-object.cjs +82 -0
  40. package/dist/export/svg/raster-object.js +81 -0
  41. package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
  42. package/dist/{pdf → export/svg}/text-decorations.js +3 -3
  43. package/dist/export/svg/text-on-path.cjs +0 -10
  44. package/dist/export/svg/text-on-path.js +1 -10
  45. package/dist/fabric/fabric-adapter.cjs +5 -3
  46. package/dist/fabric/fabric-adapter.js +5 -3
  47. package/dist/fabric/object-registry.cjs +7 -1
  48. package/dist/fabric/object-registry.js +7 -1
  49. package/dist/fabric/page-state.cjs +69 -0
  50. package/dist/fabric/page-state.js +64 -0
  51. package/dist/filters/filter-worker.cjs +281 -0
  52. package/dist/filters/filter-worker.d.cts +26 -0
  53. package/dist/filters/filter-worker.d.ts +26 -0
  54. package/dist/filters/filter-worker.js +281 -0
  55. package/dist/filters/pipeline.cjs +91 -0
  56. package/dist/filters/pipeline.d.cts +35 -0
  57. package/dist/filters/pipeline.d.ts +35 -0
  58. package/dist/filters/pipeline.js +87 -0
  59. package/dist/filters/worker-entry.cjs +78 -0
  60. package/dist/filters/worker-entry.d.cts +29 -0
  61. package/dist/filters/worker-entry.d.ts +29 -0
  62. package/dist/filters/worker-entry.js +78 -0
  63. package/dist/filters.cjs +9 -0
  64. package/dist/filters.d.cts +3 -0
  65. package/dist/filters.d.ts +3 -0
  66. package/dist/filters.js +3 -0
  67. package/dist/history/apply-state.cjs +19 -2
  68. package/dist/history/apply-state.js +19 -2
  69. package/dist/history/create-history.cjs +53 -13
  70. package/dist/history/create-history.d.cts +8 -0
  71. package/dist/history/create-history.d.ts +8 -0
  72. package/dist/history/create-history.js +53 -13
  73. package/dist/history/snapshot.cjs +10 -1
  74. package/dist/history/snapshot.js +10 -2
  75. package/dist/import/svg-import.cjs +102 -0
  76. package/dist/import/svg-import.d.cts +1 -0
  77. package/dist/import/svg-import.d.ts +1 -0
  78. package/dist/import/svg-import.js +103 -1
  79. package/dist/index.cjs +7 -0
  80. package/dist/index.d.cts +4 -3
  81. package/dist/index.d.ts +4 -3
  82. package/dist/index.js +2 -1
  83. package/dist/migrations/migrate-document.cjs +2 -1
  84. package/dist/migrations/migrate-document.js +2 -1
  85. package/dist/pdf/export-pdf.cjs +8 -67
  86. package/dist/pdf/export-pdf.js +8 -67
  87. package/dist/performance/batch.cjs +31 -0
  88. package/dist/performance/batch.d.cts +4 -0
  89. package/dist/performance/batch.d.ts +4 -0
  90. package/dist/performance/batch.js +31 -0
  91. package/dist/performance/dirty-regions.cjs +378 -0
  92. package/dist/performance/dirty-regions.d.cts +23 -0
  93. package/dist/performance/dirty-regions.d.ts +23 -0
  94. package/dist/performance/dirty-regions.js +378 -0
  95. package/dist/performance/monitor.cjs +60 -0
  96. package/dist/performance/monitor.d.cts +20 -0
  97. package/dist/performance/monitor.d.ts +20 -0
  98. package/dist/performance/monitor.js +60 -0
  99. package/dist/performance/spatial-index.cjs +78 -0
  100. package/dist/performance/spatial-index.d.cts +21 -0
  101. package/dist/performance/spatial-index.d.ts +21 -0
  102. package/dist/performance/spatial-index.js +78 -0
  103. package/dist/performance.cjs +9 -0
  104. package/dist/performance.d.cts +5 -0
  105. package/dist/performance.d.ts +5 -0
  106. package/dist/performance.js +5 -0
  107. package/dist/recovery/recovery-controller.cjs +148 -29
  108. package/dist/recovery/recovery-controller.d.cts +8 -0
  109. package/dist/recovery/recovery-controller.d.ts +8 -0
  110. package/dist/recovery/recovery-controller.js +148 -29
  111. package/dist/security/content-limits.cjs +43 -3
  112. package/dist/security/content-limits.d.cts +8 -0
  113. package/dist/security/content-limits.d.ts +8 -0
  114. package/dist/security/content-limits.js +39 -4
  115. package/dist/text/bounded-textbox.cjs +306 -0
  116. package/dist/text/bounded-textbox.d.cts +53 -0
  117. package/dist/text/bounded-textbox.d.ts +53 -0
  118. package/dist/text/bounded-textbox.js +306 -0
  119. package/dist/text/mobile-input.cjs +146 -0
  120. package/dist/text/mobile-input.d.cts +19 -0
  121. package/dist/text/mobile-input.d.ts +19 -0
  122. package/dist/text/mobile-input.js +145 -0
  123. package/dist/text/register.cjs +17 -0
  124. package/dist/text/register.d.cts +5 -0
  125. package/dist/text/register.d.ts +5 -0
  126. package/dist/text/register.js +16 -0
  127. package/dist/text/shaping.cjs +125 -0
  128. package/dist/text/shaping.d.cts +28 -0
  129. package/dist/text/shaping.d.ts +28 -0
  130. package/dist/text/shaping.js +121 -0
  131. package/dist/text/vertical-orientation.cjs +192 -0
  132. package/dist/text/vertical-orientation.d.cts +10 -0
  133. package/dist/text/vertical-orientation.d.ts +10 -0
  134. package/dist/text/vertical-orientation.js +190 -0
  135. package/dist/text/vertical-text.cjs +336 -0
  136. package/dist/text/vertical-text.d.cts +54 -0
  137. package/dist/text/vertical-text.d.ts +54 -0
  138. package/dist/text/vertical-text.js +336 -0
  139. package/dist/text.cjs +21 -0
  140. package/dist/text.d.cts +7 -0
  141. package/dist/text.d.ts +7 -0
  142. package/dist/text.js +7 -0
  143. package/dist/util/concurrency.cjs +16 -3
  144. package/dist/util/concurrency.js +16 -3
  145. package/package.json +31 -1
  146. package/schema/document-v1.schema.json +5 -1
@@ -0,0 +1,35 @@
1
+ //#region src/filters/pipeline.d.ts
2
+ export declare const PIXEL_FILTERS: ReadonlySet<string>;
3
+ export declare const WORKER_FILTERS: ReadonlySet<string>;
4
+ export interface PipelineFilter {
5
+ type: string;
6
+ applyTo2d(state: PipelineState): void;
7
+ isNeutralState?(): boolean;
8
+ subFilters?: PipelineFilter[];
9
+ }
10
+ export interface PipelineState {
11
+ sourceWidth: number;
12
+ sourceHeight: number;
13
+ imageData: ImageData;
14
+ originalImageData: ImageData;
15
+ originalEl: unknown;
16
+ canvasEl: unknown;
17
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
18
+ filterBackend: unknown;
19
+ }
20
+ export interface PipelineOptions {
21
+ bandRows?: number;
22
+ onProgress?: (done: number) => void;
23
+ isCancelled?: () => boolean;
24
+ pause?: () => Promise<void>;
25
+ }
26
+ export declare function flattenFilters<Filter extends {
27
+ type?: unknown;
28
+ subFilters?: Filter[];
29
+ }>(filters: readonly Filter[]): Filter[];
30
+ export declare function canRunInWorker(filters: ReadonlyArray<{
31
+ type?: unknown;
32
+ subFilters?: unknown[];
33
+ }>): boolean;
34
+ export declare function runFilterPipeline(filters: readonly PipelineFilter[], state: PipelineState, options?: PipelineOptions): Promise<ImageData | undefined>;
35
+ //#endregion
@@ -0,0 +1,35 @@
1
+ //#region src/filters/pipeline.d.ts
2
+ export declare const PIXEL_FILTERS: ReadonlySet<string>;
3
+ export declare const WORKER_FILTERS: ReadonlySet<string>;
4
+ export interface PipelineFilter {
5
+ type: string;
6
+ applyTo2d(state: PipelineState): void;
7
+ isNeutralState?(): boolean;
8
+ subFilters?: PipelineFilter[];
9
+ }
10
+ export interface PipelineState {
11
+ sourceWidth: number;
12
+ sourceHeight: number;
13
+ imageData: ImageData;
14
+ originalImageData: ImageData;
15
+ originalEl: unknown;
16
+ canvasEl: unknown;
17
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
18
+ filterBackend: unknown;
19
+ }
20
+ export interface PipelineOptions {
21
+ bandRows?: number;
22
+ onProgress?: (done: number) => void;
23
+ isCancelled?: () => boolean;
24
+ pause?: () => Promise<void>;
25
+ }
26
+ export declare function flattenFilters<Filter extends {
27
+ type?: unknown;
28
+ subFilters?: Filter[];
29
+ }>(filters: readonly Filter[]): Filter[];
30
+ export declare function canRunInWorker(filters: ReadonlyArray<{
31
+ type?: unknown;
32
+ subFilters?: unknown[];
33
+ }>): boolean;
34
+ export declare function runFilterPipeline(filters: readonly PipelineFilter[], state: PipelineState, options?: PipelineOptions): Promise<ImageData | undefined>;
35
+ //#endregion
@@ -0,0 +1,87 @@
1
+ //#region src/filters/pipeline.ts
2
+ const PIXEL_FILTERS = /* @__PURE__ */ new Set([
3
+ "Brightness",
4
+ "Contrast",
5
+ "Saturation",
6
+ "Vibrance",
7
+ "Grayscale",
8
+ "Invert",
9
+ "Gamma",
10
+ "Noise",
11
+ "RemoveColor",
12
+ "BlendColor",
13
+ "HueRotation",
14
+ "ColorMatrix",
15
+ "Sepia",
16
+ "Brownie",
17
+ "Vintage",
18
+ "Kodachrome",
19
+ "Technicolor",
20
+ "Polaroid",
21
+ "BlackWhite"
22
+ ]);
23
+ const WORKER_FILTERS = /* @__PURE__ */ new Set([
24
+ ...PIXEL_FILTERS,
25
+ "Blur",
26
+ "Convolute",
27
+ "Pixelate"
28
+ ]);
29
+ function typeOf(filter) {
30
+ const own = filter.type ?? filter.constructor?.type;
31
+ return typeof own === "string" ? own : "";
32
+ }
33
+ function flattenFilters(filters) {
34
+ const flat = [];
35
+ const visit = (filter) => {
36
+ if (typeOf(filter) === "Composed") (filter.subFilters ?? []).forEach(visit);
37
+ else flat.push(filter);
38
+ };
39
+ filters.forEach(visit);
40
+ return flat;
41
+ }
42
+ function canRunInWorker(filters) {
43
+ return flattenFilters(filters).every((filter) => WORKER_FILTERS.has(typeOf(filter)));
44
+ }
45
+ async function runFilterPipeline(filters, state, options = {}) {
46
+ const steps = [];
47
+ for (const filter of flattenFilters(filters)) {
48
+ if (filter.isNeutralState?.()) continue;
49
+ const last = steps[steps.length - 1];
50
+ if (PIXEL_FILTERS.has(typeOf(filter)) && last && PIXEL_FILTERS.has(typeOf(last[0]))) last.push(filter);
51
+ else steps.push([filter]);
52
+ }
53
+ const bandRows = Math.max(1, Math.floor(options.bandRows ?? 64));
54
+ const units = steps.reduce((sum, step) => sum + (PIXEL_FILTERS.has(typeOf(step[0])) ? Math.ceil(state.imageData.height / bandRows) : 1), 0);
55
+ let done = 0;
56
+ const advance = async () => {
57
+ done += 1;
58
+ options.onProgress?.(units === 0 ? 1 : done / units);
59
+ if (options.isCancelled?.()) return false;
60
+ await options.pause?.();
61
+ return !options.isCancelled?.();
62
+ };
63
+ for (const step of steps) {
64
+ if (!PIXEL_FILTERS.has(typeOf(step[0]))) {
65
+ step[0].applyTo2d(state);
66
+ if (!await advance()) return void 0;
67
+ continue;
68
+ }
69
+ const whole = state.imageData;
70
+ const rowBytes = whole.width * 4;
71
+ for (let row = 0; row < whole.height; row += bandRows) {
72
+ const rows = Math.min(bandRows, whole.height - row);
73
+ const band = new ImageData(new Uint8ClampedArray(whole.data.buffer, whole.data.byteOffset + row * rowBytes, rows * rowBytes), whole.width, rows);
74
+ const bandState = {
75
+ ...state,
76
+ imageData: band,
77
+ sourceHeight: rows
78
+ };
79
+ for (const filter of step) filter.applyTo2d(bandState);
80
+ if (!await advance()) return void 0;
81
+ }
82
+ }
83
+ if (units === 0) options.onProgress?.(1);
84
+ return state.imageData;
85
+ }
86
+ //#endregion
87
+ export { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline };
@@ -0,0 +1,78 @@
1
+ const require_pipeline = require("./pipeline.cjs");
2
+ let fabric = require("fabric");
3
+ //#region src/filters/worker-entry.ts
4
+ const scope = self;
5
+ const running = /* @__PURE__ */ new Set();
6
+ const cancelled = /* @__PURE__ */ new Set();
7
+ const channel = new MessageChannel();
8
+ const waiting = [];
9
+ channel.port1.onmessage = () => waiting.shift()?.();
10
+ const pause = () => new Promise((resolve) => {
11
+ waiting.push(resolve);
12
+ channel.port2.postMessage(null);
13
+ });
14
+ async function apply(message) {
15
+ const { id, bitmap } = message;
16
+ running.add(id);
17
+ try {
18
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
19
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
20
+ ctx.drawImage(bitmap, 0, 0);
21
+ bitmap.close();
22
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
23
+ const filters = await Promise.all(message.filters.map((json) => fabric.classRegistry.getClass(String(json.type)).fromObject(json)));
24
+ const result = await require_pipeline.runFilterPipeline(filters, {
25
+ sourceWidth: canvas.width,
26
+ sourceHeight: canvas.height,
27
+ imageData,
28
+ originalImageData: imageData,
29
+ originalEl: canvas,
30
+ canvasEl: canvas,
31
+ ctx,
32
+ filterBackend: void 0
33
+ }, {
34
+ bandRows: message.bandRows,
35
+ pause,
36
+ isCancelled: () => cancelled.has(id),
37
+ onProgress: (done) => scope.postMessage({
38
+ type: "progress",
39
+ id,
40
+ done
41
+ })
42
+ });
43
+ if (!result) {
44
+ scope.postMessage({
45
+ type: "cancelled",
46
+ id
47
+ });
48
+ return;
49
+ }
50
+ if (result.width !== canvas.width || result.height !== canvas.height) {
51
+ canvas.width = result.width;
52
+ canvas.height = result.height;
53
+ }
54
+ ctx.putImageData(result, 0, 0);
55
+ const output = canvas.transferToImageBitmap();
56
+ scope.postMessage({
57
+ type: "done",
58
+ id,
59
+ bitmap: output
60
+ }, [output]);
61
+ } catch (error) {
62
+ scope.postMessage({
63
+ type: "error",
64
+ id,
65
+ message: error instanceof Error ? error.message : String(error)
66
+ });
67
+ } finally {
68
+ running.delete(id);
69
+ cancelled.delete(id);
70
+ }
71
+ }
72
+ scope.onmessage = (event) => {
73
+ const message = event.data;
74
+ if (message.type === "cancel") {
75
+ if (running.has(message.id)) cancelled.add(message.id);
76
+ } else apply(message);
77
+ };
78
+ //#endregion
@@ -0,0 +1,29 @@
1
+ //#region src/filters/worker-entry.d.ts
2
+ export interface ApplyMessage {
3
+ type: "apply";
4
+ id: number;
5
+ bitmap: ImageBitmap;
6
+ filters: Array<Record<string, unknown>>;
7
+ bandRows?: number;
8
+ }
9
+ export interface CancelMessage {
10
+ type: "cancel";
11
+ id: number;
12
+ }
13
+ export type WorkerReply = {
14
+ type: "progress";
15
+ id: number;
16
+ done: number;
17
+ } | {
18
+ type: "done";
19
+ id: number;
20
+ bitmap: ImageBitmap;
21
+ } | {
22
+ type: "cancelled";
23
+ id: number;
24
+ } | {
25
+ type: "error";
26
+ id: number;
27
+ message: string;
28
+ };
29
+ //#endregion
@@ -0,0 +1,29 @@
1
+ //#region src/filters/worker-entry.d.ts
2
+ export interface ApplyMessage {
3
+ type: "apply";
4
+ id: number;
5
+ bitmap: ImageBitmap;
6
+ filters: Array<Record<string, unknown>>;
7
+ bandRows?: number;
8
+ }
9
+ export interface CancelMessage {
10
+ type: "cancel";
11
+ id: number;
12
+ }
13
+ export type WorkerReply = {
14
+ type: "progress";
15
+ id: number;
16
+ done: number;
17
+ } | {
18
+ type: "done";
19
+ id: number;
20
+ bitmap: ImageBitmap;
21
+ } | {
22
+ type: "cancelled";
23
+ id: number;
24
+ } | {
25
+ type: "error";
26
+ id: number;
27
+ message: string;
28
+ };
29
+ //#endregion
@@ -0,0 +1,78 @@
1
+ import { runFilterPipeline } from "./pipeline.js";
2
+ import { classRegistry } from "fabric";
3
+ //#region src/filters/worker-entry.ts
4
+ const scope = self;
5
+ const running = /* @__PURE__ */ new Set();
6
+ const cancelled = /* @__PURE__ */ new Set();
7
+ const channel = new MessageChannel();
8
+ const waiting = [];
9
+ channel.port1.onmessage = () => waiting.shift()?.();
10
+ const pause = () => new Promise((resolve) => {
11
+ waiting.push(resolve);
12
+ channel.port2.postMessage(null);
13
+ });
14
+ async function apply(message) {
15
+ const { id, bitmap } = message;
16
+ running.add(id);
17
+ try {
18
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
19
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
20
+ ctx.drawImage(bitmap, 0, 0);
21
+ bitmap.close();
22
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
23
+ const filters = await Promise.all(message.filters.map((json) => classRegistry.getClass(String(json.type)).fromObject(json)));
24
+ const result = await runFilterPipeline(filters, {
25
+ sourceWidth: canvas.width,
26
+ sourceHeight: canvas.height,
27
+ imageData,
28
+ originalImageData: imageData,
29
+ originalEl: canvas,
30
+ canvasEl: canvas,
31
+ ctx,
32
+ filterBackend: void 0
33
+ }, {
34
+ bandRows: message.bandRows,
35
+ pause,
36
+ isCancelled: () => cancelled.has(id),
37
+ onProgress: (done) => scope.postMessage({
38
+ type: "progress",
39
+ id,
40
+ done
41
+ })
42
+ });
43
+ if (!result) {
44
+ scope.postMessage({
45
+ type: "cancelled",
46
+ id
47
+ });
48
+ return;
49
+ }
50
+ if (result.width !== canvas.width || result.height !== canvas.height) {
51
+ canvas.width = result.width;
52
+ canvas.height = result.height;
53
+ }
54
+ ctx.putImageData(result, 0, 0);
55
+ const output = canvas.transferToImageBitmap();
56
+ scope.postMessage({
57
+ type: "done",
58
+ id,
59
+ bitmap: output
60
+ }, [output]);
61
+ } catch (error) {
62
+ scope.postMessage({
63
+ type: "error",
64
+ id,
65
+ message: error instanceof Error ? error.message : String(error)
66
+ });
67
+ } finally {
68
+ running.delete(id);
69
+ cancelled.delete(id);
70
+ }
71
+ }
72
+ scope.onmessage = (event) => {
73
+ const message = event.data;
74
+ if (message.type === "cancel") {
75
+ if (running.has(message.id)) cancelled.add(message.id);
76
+ } else apply(message);
77
+ };
78
+ //#endregion
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_pipeline = require("./filters/pipeline.cjs");
3
+ const require_filter_worker = require("./filters/filter-worker.cjs");
4
+ exports.PIXEL_FILTERS = require_pipeline.PIXEL_FILTERS;
5
+ exports.WORKER_FILTERS = require_pipeline.WORKER_FILTERS;
6
+ exports.canRunInWorker = require_pipeline.canRunInWorker;
7
+ exports.createFilterWorker = require_filter_worker.createFilterWorker;
8
+ exports.flattenFilters = require_pipeline.flattenFilters;
9
+ exports.runFilterPipeline = require_pipeline.runFilterPipeline;
@@ -0,0 +1,3 @@
1
+ import { ApplyFilterOptions, FilterWorker, FilterWorkerOptions, ImageFilter, createFilterWorker } from "./filters/filter-worker.cjs";
2
+ import { PIXEL_FILTERS, PipelineFilter, PipelineOptions, PipelineState, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.cjs";
3
+ export { type ApplyFilterOptions, type FilterWorker, type FilterWorkerOptions, type ImageFilter, PIXEL_FILTERS, type PipelineFilter, type PipelineOptions, type PipelineState, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
@@ -0,0 +1,3 @@
1
+ import { ApplyFilterOptions, FilterWorker, FilterWorkerOptions, ImageFilter, createFilterWorker } from "./filters/filter-worker.js";
2
+ import { PIXEL_FILTERS, PipelineFilter, PipelineOptions, PipelineState, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.js";
3
+ export { type ApplyFilterOptions, type FilterWorker, type FilterWorkerOptions, type ImageFilter, PIXEL_FILTERS, type PipelineFilter, type PipelineOptions, type PipelineState, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
@@ -0,0 +1,3 @@
1
+ import { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.js";
2
+ import { createFilterWorker } from "./filters/filter-worker.js";
3
+ export { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
@@ -1,5 +1,7 @@
1
+ const require_page_state = require("../fabric/page-state.cjs");
1
2
  const require_object_ids = require("../fabric/object-ids.cjs");
2
3
  const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
4
+ const require_snapshot = require("./snapshot.cjs");
3
5
  //#region src/history/apply-state.ts
4
6
  function arrangeObjects(current, change, created) {
5
7
  const arranged = [];
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
20
22
  const position = new Map(change.order.map((id, index) => [id, index]));
21
23
  return arranged.sort((first, second) => (position.get(require_object_ids.readObjectId(first) ?? "") ?? Infinity) - (position.get(require_object_ids.readObjectId(second) ?? "") ?? Infinity));
22
24
  }
23
- async function applyStateChange(canvas, change) {
25
+ /**
26
+ * Applies a change. `isStale` is checked after every wait: when it returns
27
+ * true the canvas now shows another document, so nothing is applied and the
28
+ * call returns false.
29
+ */
30
+ async function applyStateChange(canvas, change, isStale = () => false) {
31
+ const page = change.objects.get(require_snapshot.PAGE_KEY);
32
+ if (page) {
33
+ if (isStale()) return false;
34
+ await require_page_state.applyPage(canvas, JSON.parse(page));
35
+ }
24
36
  const serializedObjects = [];
25
- for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
37
+ for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
26
38
  const createdObjects = await require_fabric_adapter.createObjects(serializedObjects);
39
+ if (isStale()) {
40
+ createdObjects.forEach((object) => object.dispose?.());
41
+ return false;
42
+ }
27
43
  const createdById = new Map(createdObjects.map((object) => [require_object_ids.readObjectId(object) ?? "", object]));
28
44
  canvas.discardActiveObject?.();
29
45
  const current = canvas.getObjects();
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
45
61
  canvas.renderOnAddRemove = renderOnAddRemove;
46
62
  }
47
63
  canvas.requestRenderAll();
64
+ return true;
48
65
  }
49
66
  //#endregion
50
67
  exports.applyStateChange = applyStateChange;
@@ -1,5 +1,7 @@
1
+ import { applyPage } from "../fabric/page-state.js";
1
2
  import { readObjectId } from "../fabric/object-ids.js";
2
3
  import { createObjects } from "../fabric/fabric-adapter.js";
4
+ import { PAGE_KEY } from "./snapshot.js";
3
5
  //#region src/history/apply-state.ts
4
6
  function arrangeObjects(current, change, created) {
5
7
  const arranged = [];
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
20
22
  const position = new Map(change.order.map((id, index) => [id, index]));
21
23
  return arranged.sort((first, second) => (position.get(readObjectId(first) ?? "") ?? Infinity) - (position.get(readObjectId(second) ?? "") ?? Infinity));
22
24
  }
23
- async function applyStateChange(canvas, change) {
25
+ /**
26
+ * Applies a change. `isStale` is checked after every wait: when it returns
27
+ * true the canvas now shows another document, so nothing is applied and the
28
+ * call returns false.
29
+ */
30
+ async function applyStateChange(canvas, change, isStale = () => false) {
31
+ const page = change.objects.get(PAGE_KEY);
32
+ if (page) {
33
+ if (isStale()) return false;
34
+ await applyPage(canvas, JSON.parse(page));
35
+ }
24
36
  const serializedObjects = [];
25
- for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
37
+ for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
26
38
  const createdObjects = await createObjects(serializedObjects);
39
+ if (isStale()) {
40
+ createdObjects.forEach((object) => object.dispose?.());
41
+ return false;
42
+ }
27
43
  const createdById = new Map(createdObjects.map((object) => [readObjectId(object) ?? "", object]));
28
44
  canvas.discardActiveObject?.();
29
45
  const current = canvas.getObjects();
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
45
61
  canvas.renderOnAddRemove = renderOnAddRemove;
46
62
  }
47
63
  canvas.requestRenderAll();
64
+ return true;
48
65
  }
49
66
  //#endregion
50
67
  export { applyStateChange };
@@ -1,6 +1,6 @@
1
+ const require_snapshot = require("./snapshot.cjs");
1
2
  const require_apply_state = require("./apply-state.cjs");
2
3
  const require_history_stack = require("./history-stack.cjs");
3
- const require_snapshot = require("./snapshot.cjs");
4
4
  //#region src/history/create-history.ts
5
5
  const verbsByAction = {
6
6
  drag: "Move",
@@ -24,18 +24,23 @@ function describePendingChanges(changes) {
24
24
  return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
25
25
  }
26
26
  function createHistory(options) {
27
- const { canvas, serializeObjects, onChange, onContentChange } = options;
27
+ const { canvas, serializeState, onChange, onContentChange, onLiveChange } = options;
28
+ const takeSnapshot = () => {
29
+ const { objects, page } = serializeState();
30
+ return require_snapshot.createSnapshot(objects, page);
31
+ };
28
32
  const stack = require_history_stack.createHistoryStack({
29
33
  steps: Math.max(1, options.limit ?? 100),
30
34
  bytes: Math.max(0, options.maxBytes ?? 67108864)
31
35
  });
32
- let snapshot = require_snapshot.createSnapshot(serializeObjects());
36
+ let snapshot = takeSnapshot();
33
37
  let pendingChanges = [];
34
38
  let flushScheduled = false;
35
39
  let transactionDepth = 0;
36
40
  let pausedDepth = 0;
37
41
  let applying = false;
38
42
  let queue = Promise.resolve();
43
+ let epoch = 0;
39
44
  function state() {
40
45
  const undoLabels = stack.undoLabels();
41
46
  const redoLabels = stack.redoLabels();
@@ -48,7 +53,7 @@ function createHistory(options) {
48
53
  }
49
54
  function commit(label) {
50
55
  pendingChanges = [];
51
- const nextSnapshot = require_snapshot.createSnapshot(serializeObjects());
56
+ const nextSnapshot = takeSnapshot();
52
57
  const difference = require_snapshot.diffSnapshots(snapshot, nextSnapshot);
53
58
  snapshot = nextSnapshot;
54
59
  if (difference === null) return false;
@@ -86,26 +91,56 @@ function createHistory(options) {
86
91
  notice(verbsByAction[action] ?? "Transform", event.target);
87
92
  };
88
93
  const handleTextEdited = ({ target }) => notice("Edit", target);
94
+ const handleTextChanged = () => {
95
+ if (!applying && pausedDepth === 0) onLiveChange?.();
96
+ };
89
97
  const listeners = [
90
98
  ["object:added", handleAdded],
91
99
  ["object:removed", handleRemoved],
92
100
  ["object:modified", handleModified],
93
- ["text:editing:exited", handleTextEdited]
101
+ ["text:editing:exited", handleTextEdited],
102
+ ["text:changed", handleTextChanged]
94
103
  ];
95
104
  const eventTarget = canvas;
96
105
  listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
97
- function transaction(label, work) {
106
+ /**
107
+ * Puts the canvas back to the last recorded snapshot. The snapshot is the
108
+ * state before the transaction, so the difference to undo is exactly what
109
+ * the transaction changed: one serialization, then only the changed
110
+ * objects are rebuilt.
111
+ */
112
+ function rollBack() {
113
+ const difference = require_snapshot.diffSnapshots(snapshot, takeSnapshot());
114
+ pendingChanges = [];
115
+ if (difference === null) return Promise.resolve();
116
+ applying = true;
117
+ return runInOrder(async () => {
118
+ try {
119
+ await require_apply_state.applyStateChange(canvas, difference.before);
120
+ } finally {
121
+ applying = false;
122
+ }
123
+ });
124
+ }
125
+ function transaction(label, work, transactionOptions = {}) {
98
126
  if (transactionDepth === 0) commitPendingChanges();
127
+ const outermost = transactionDepth === 0;
99
128
  transactionDepth += 1;
100
129
  const finish = () => {
101
130
  transactionDepth -= 1;
102
131
  if (transactionDepth === 0) commit(label);
103
132
  };
133
+ const fail = () => {
134
+ transactionDepth -= 1;
135
+ if (transactionDepth > 0) return void 0;
136
+ if (outermost && transactionOptions.rollback) return rollBack();
137
+ commit(label);
138
+ };
104
139
  let result;
105
140
  try {
106
141
  result = work();
107
142
  } catch (error) {
108
- finish();
143
+ fail()?.catch(() => void 0);
109
144
  throw error;
110
145
  }
111
146
  if (!isPromiseLike(result)) {
@@ -115,8 +150,8 @@ function createHistory(options) {
115
150
  return Promise.resolve(result).then((value) => {
116
151
  finish();
117
152
  return value;
118
- }, (error) => {
119
- finish();
153
+ }, async (error) => {
154
+ await fail();
120
155
  throw error;
121
156
  });
122
157
  }
@@ -148,16 +183,19 @@ function createHistory(options) {
148
183
  commitPendingChanges();
149
184
  const step = takeStep();
150
185
  if (step === void 0) return false;
186
+ const startedIn = epoch;
151
187
  applying = true;
188
+ let applied;
152
189
  try {
153
- await require_apply_state.applyStateChange(canvas, pickChange(step));
190
+ applied = await require_apply_state.applyStateChange(canvas, pickChange(step), () => epoch !== startedIn);
154
191
  } catch (error) {
155
- putBack(step);
192
+ if (epoch === startedIn) putBack(step);
156
193
  throw error;
157
194
  } finally {
158
195
  applying = false;
159
196
  }
160
- snapshot = require_snapshot.createSnapshot(serializeObjects());
197
+ if (!applied) return false;
198
+ snapshot = takeSnapshot();
161
199
  moveTo(step);
162
200
  onContentChange();
163
201
  onChange(state());
@@ -170,6 +208,7 @@ function createHistory(options) {
170
208
  return commit(label);
171
209
  },
172
210
  transaction,
211
+ settled: () => queue.then(() => void 0),
173
212
  undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
174
213
  redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
175
214
  state,
@@ -182,7 +221,8 @@ function createHistory(options) {
182
221
  reset() {
183
222
  pendingChanges = [];
184
223
  stack.clear();
185
- snapshot = require_snapshot.createSnapshot(serializeObjects());
224
+ epoch += 1;
225
+ snapshot = takeSnapshot();
186
226
  onChange(state());
187
227
  },
188
228
  destroy() {
@@ -10,4 +10,12 @@ export interface HistoryState {
10
10
  undoLabel: string | undefined;
11
11
  redoLabel: string | undefined;
12
12
  }
13
+ export interface TransactionOptions {
14
+ /**
15
+ * When `work` throws or rejects, put the canvas back as it was before the
16
+ * transaction, record no undo step and leave the document's unsaved state
17
+ * alone. Applies to the outermost transaction.
18
+ */
19
+ rollback?: boolean;
20
+ }
13
21
  //#endregion
@@ -10,4 +10,12 @@ export interface HistoryState {
10
10
  undoLabel: string | undefined;
11
11
  redoLabel: string | undefined;
12
12
  }
13
+ export interface TransactionOptions {
14
+ /**
15
+ * When `work` throws or rejects, put the canvas back as it was before the
16
+ * transaction, record no undo step and leave the document's unsaved state
17
+ * alone. Applies to the outermost transaction.
18
+ */
19
+ rollback?: boolean;
20
+ }
13
21
  //#endregion