fabricjs-document-engine 0.0.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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/document/create-document.cjs +13 -0
- package/dist/document/create-document.d.cts +6 -0
- package/dist/document/create-document.d.ts +6 -0
- package/dist/document/create-document.js +13 -0
- package/dist/document/document-format.cjs +4 -0
- package/dist/document/document-format.d.cts +31 -0
- package/dist/document/document-format.d.ts +31 -0
- package/dist/document/document-format.js +4 -0
- package/dist/document/ids.cjs +11 -0
- package/dist/document/ids.js +11 -0
- package/dist/document/validate-document.cjs +70 -0
- package/dist/document/validate-document.d.cts +4 -0
- package/dist/document/validate-document.d.ts +4 -0
- package/dist/document/validate-document.js +70 -0
- package/dist/engine/create-document-engine.cjs +218 -0
- package/dist/engine/create-document-engine.d.cts +56 -0
- package/dist/engine/create-document-engine.d.ts +56 -0
- package/dist/engine/create-document-engine.js +218 -0
- package/dist/engine/errors.cjs +17 -0
- package/dist/engine/errors.d.cts +21 -0
- package/dist/engine/errors.d.ts +21 -0
- package/dist/engine/errors.js +16 -0
- package/dist/engine/event-emitter.cjs +24 -0
- package/dist/engine/event-emitter.d.cts +3 -0
- package/dist/engine/event-emitter.d.ts +3 -0
- package/dist/engine/event-emitter.js +24 -0
- package/dist/fabric/fabric-adapter.cjs +68 -0
- package/dist/fabric/fabric-adapter.js +67 -0
- package/dist/fabric/object-ids.cjs +11 -0
- package/dist/fabric/object-ids.js +10 -0
- package/dist/fabric/object-registry.cjs +21 -0
- package/dist/fabric/object-registry.d.cts +9 -0
- package/dist/fabric/object-registry.d.ts +9 -0
- package/dist/fabric/object-registry.js +21 -0
- package/dist/fabric/walk-objects.cjs +29 -0
- package/dist/fabric/walk-objects.js +27 -0
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/package.json +91 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
|
|
2
|
+
import { NewDocumentOptions } from "../document/create-document.cjs";
|
|
3
|
+
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
4
|
+
import { DocumentEngineError } from "./errors.cjs";
|
|
5
|
+
import { Unsubscribe } from "./event-emitter.cjs";
|
|
6
|
+
import { FabricObject, StaticCanvas } from "fabric";
|
|
7
|
+
//#region src/engine/create-document-engine.d.ts
|
|
8
|
+
export interface DocumentStorage {
|
|
9
|
+
loadDocument(id: string): Promise<unknown>;
|
|
10
|
+
saveDocument(document: FabricDocument): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface DocumentEngineOptions {
|
|
13
|
+
canvas: StaticCanvas;
|
|
14
|
+
storage?: DocumentStorage;
|
|
15
|
+
customObjects?: CustomObjectDefinition[];
|
|
16
|
+
document?: NewDocumentOptions;
|
|
17
|
+
}
|
|
18
|
+
export interface LoadOptions {
|
|
19
|
+
restoreCanvasSize?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DocumentEngineEvents {
|
|
22
|
+
"load:start": {
|
|
23
|
+
documentId: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
"load:success": {
|
|
26
|
+
document: FabricDocument;
|
|
27
|
+
};
|
|
28
|
+
"load:error": {
|
|
29
|
+
error: DocumentEngineError;
|
|
30
|
+
};
|
|
31
|
+
"save:start": {
|
|
32
|
+
document: FabricDocument;
|
|
33
|
+
};
|
|
34
|
+
"save:success": {
|
|
35
|
+
document: FabricDocument;
|
|
36
|
+
};
|
|
37
|
+
"save:error": {
|
|
38
|
+
error: DocumentEngineError;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface DocumentEngine {
|
|
42
|
+
readonly canvas: StaticCanvas;
|
|
43
|
+
getDocumentInfo(): DocumentInfo;
|
|
44
|
+
updateMetadata(changes: Record<string, unknown>): void;
|
|
45
|
+
newDocument(options?: NewDocumentOptions): void;
|
|
46
|
+
toDocument(): FabricDocument;
|
|
47
|
+
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
48
|
+
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
49
|
+
save(): Promise<FabricDocument>;
|
|
50
|
+
registerObject(definition: CustomObjectDefinition): void;
|
|
51
|
+
getObjectById(id: string): FabricObject | undefined;
|
|
52
|
+
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function createDocumentEngine(options: DocumentEngineOptions): DocumentEngine;
|
|
56
|
+
//#endregion
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DocumentInfo, FabricDocument } from "../document/document-format.js";
|
|
2
|
+
import { NewDocumentOptions } from "../document/create-document.js";
|
|
3
|
+
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
4
|
+
import { DocumentEngineError } from "./errors.js";
|
|
5
|
+
import { Unsubscribe } from "./event-emitter.js";
|
|
6
|
+
import { FabricObject, StaticCanvas } from "fabric";
|
|
7
|
+
//#region src/engine/create-document-engine.d.ts
|
|
8
|
+
export interface DocumentStorage {
|
|
9
|
+
loadDocument(id: string): Promise<unknown>;
|
|
10
|
+
saveDocument(document: FabricDocument): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface DocumentEngineOptions {
|
|
13
|
+
canvas: StaticCanvas;
|
|
14
|
+
storage?: DocumentStorage;
|
|
15
|
+
customObjects?: CustomObjectDefinition[];
|
|
16
|
+
document?: NewDocumentOptions;
|
|
17
|
+
}
|
|
18
|
+
export interface LoadOptions {
|
|
19
|
+
restoreCanvasSize?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DocumentEngineEvents {
|
|
22
|
+
"load:start": {
|
|
23
|
+
documentId: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
"load:success": {
|
|
26
|
+
document: FabricDocument;
|
|
27
|
+
};
|
|
28
|
+
"load:error": {
|
|
29
|
+
error: DocumentEngineError;
|
|
30
|
+
};
|
|
31
|
+
"save:start": {
|
|
32
|
+
document: FabricDocument;
|
|
33
|
+
};
|
|
34
|
+
"save:success": {
|
|
35
|
+
document: FabricDocument;
|
|
36
|
+
};
|
|
37
|
+
"save:error": {
|
|
38
|
+
error: DocumentEngineError;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface DocumentEngine {
|
|
42
|
+
readonly canvas: StaticCanvas;
|
|
43
|
+
getDocumentInfo(): DocumentInfo;
|
|
44
|
+
updateMetadata(changes: Record<string, unknown>): void;
|
|
45
|
+
newDocument(options?: NewDocumentOptions): void;
|
|
46
|
+
toDocument(): FabricDocument;
|
|
47
|
+
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
48
|
+
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
49
|
+
save(): Promise<FabricDocument>;
|
|
50
|
+
registerObject(definition: CustomObjectDefinition): void;
|
|
51
|
+
getObjectById(id: string): FabricObject | undefined;
|
|
52
|
+
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function createDocumentEngine(options: DocumentEngineOptions): DocumentEngine;
|
|
56
|
+
//#endregion
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import "../document/document-format.js";
|
|
2
|
+
import { createId } from "../document/ids.js";
|
|
3
|
+
import { createDocumentInfo } from "../document/create-document.js";
|
|
4
|
+
import { validateDocument } from "../document/validate-document.js";
|
|
5
|
+
import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
|
|
6
|
+
import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
|
|
7
|
+
import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
|
|
8
|
+
import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
|
|
9
|
+
import { createObjectRegistry } from "../fabric/object-registry.js";
|
|
10
|
+
import { createEventEmitter } from "./event-emitter.js";
|
|
11
|
+
//#region src/engine/create-document-engine.ts
|
|
12
|
+
function describeDocumentId(value) {
|
|
13
|
+
const id = value?.id;
|
|
14
|
+
return typeof id === "string" ? id : void 0;
|
|
15
|
+
}
|
|
16
|
+
function createDocumentEngine(options) {
|
|
17
|
+
const { canvas, storage } = options;
|
|
18
|
+
const registry = createObjectRegistry(options.customObjects);
|
|
19
|
+
const events = createEventEmitter();
|
|
20
|
+
const objectsById = /* @__PURE__ */ new Map();
|
|
21
|
+
let documentInfo = createDocumentInfo(options.document);
|
|
22
|
+
let activeLoad = null;
|
|
23
|
+
let destroyed = false;
|
|
24
|
+
function ensureUsable() {
|
|
25
|
+
if (destroyed) throw new DocumentEngineError("ENGINE_DESTROYED", "This document engine was destroyed");
|
|
26
|
+
}
|
|
27
|
+
function indexObjectTree(root) {
|
|
28
|
+
walkObjects([root], (object) => {
|
|
29
|
+
let id = readObjectId(object);
|
|
30
|
+
const owner = id === void 0 ? void 0 : objectsById.get(id);
|
|
31
|
+
if (id === void 0 || owner !== void 0 && owner !== object) {
|
|
32
|
+
id = createId();
|
|
33
|
+
writeObjectId(object, id);
|
|
34
|
+
}
|
|
35
|
+
objectsById.set(id, object);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function removeObjectTreeFromIndex(root) {
|
|
39
|
+
walkObjects([root], (object) => {
|
|
40
|
+
const id = readObjectId(object);
|
|
41
|
+
if (id !== void 0 && objectsById.get(id) === object) objectsById.delete(id);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function rebuildIndex() {
|
|
45
|
+
objectsById.clear();
|
|
46
|
+
canvas.getObjects().forEach(indexObjectTree);
|
|
47
|
+
}
|
|
48
|
+
const handleObjectAdded = ({ target }) => indexObjectTree(target);
|
|
49
|
+
const handleObjectRemoved = ({ target }) => removeObjectTreeFromIndex(target);
|
|
50
|
+
canvas.on("object:added", handleObjectAdded);
|
|
51
|
+
canvas.on("object:removed", handleObjectRemoved);
|
|
52
|
+
rebuildIndex();
|
|
53
|
+
function toDocument() {
|
|
54
|
+
ensureUsable();
|
|
55
|
+
rebuildIndex();
|
|
56
|
+
const serialized = serializeCanvas(canvas, registry.propertiesToInclude());
|
|
57
|
+
documentInfo = {
|
|
58
|
+
...documentInfo,
|
|
59
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
60
|
+
};
|
|
61
|
+
const document = {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
id: documentInfo.id,
|
|
64
|
+
createdAt: documentInfo.createdAt,
|
|
65
|
+
updatedAt: documentInfo.updatedAt,
|
|
66
|
+
canvas: {
|
|
67
|
+
width: canvas.getWidth(),
|
|
68
|
+
height: canvas.getHeight()
|
|
69
|
+
},
|
|
70
|
+
objects: serialized.objects,
|
|
71
|
+
metadata: { ...documentInfo.metadata }
|
|
72
|
+
};
|
|
73
|
+
if (serialized.version !== void 0) document.fabricVersion = serialized.version;
|
|
74
|
+
if (serialized.background !== void 0) document.canvas.background = serialized.background;
|
|
75
|
+
return document;
|
|
76
|
+
}
|
|
77
|
+
function toLoadError(error, signal) {
|
|
78
|
+
if (isDocumentEngineError(error)) return error;
|
|
79
|
+
if (signal.aborted) return new DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
|
|
80
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
81
|
+
return new DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
|
|
82
|
+
}
|
|
83
|
+
function checkDocument(input) {
|
|
84
|
+
const issues = validateDocument(input);
|
|
85
|
+
if (issues.length > 0) {
|
|
86
|
+
const summary = issues.map((issue) => `${issue.path || "document"} ${issue.message}`).join("; ");
|
|
87
|
+
throw new DocumentEngineError(issues[0].code, `The document is not valid: ${summary}`, { issues });
|
|
88
|
+
}
|
|
89
|
+
const document = input;
|
|
90
|
+
const unknownTypes = registry.findUnknownTypes(collectSerializedTypes(document.objects));
|
|
91
|
+
if (unknownTypes.length > 0) throw new DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
92
|
+
return document;
|
|
93
|
+
}
|
|
94
|
+
async function loadDocument(input, loadOptions = {}) {
|
|
95
|
+
ensureUsable();
|
|
96
|
+
activeLoad?.abort();
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
activeLoad = controller;
|
|
99
|
+
events.emit("load:start", { documentId: describeDocumentId(input) });
|
|
100
|
+
try {
|
|
101
|
+
const document = checkDocument(input);
|
|
102
|
+
await loadIntoCanvas(canvas, {
|
|
103
|
+
version: document.fabricVersion,
|
|
104
|
+
background: document.canvas.background,
|
|
105
|
+
objects: document.objects
|
|
106
|
+
}, controller.signal);
|
|
107
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
108
|
+
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
109
|
+
width: document.canvas.width,
|
|
110
|
+
height: document.canvas.height
|
|
111
|
+
});
|
|
112
|
+
documentInfo = {
|
|
113
|
+
id: document.id,
|
|
114
|
+
createdAt: document.createdAt,
|
|
115
|
+
updatedAt: document.updatedAt,
|
|
116
|
+
metadata: { ...document.metadata }
|
|
117
|
+
};
|
|
118
|
+
rebuildIndex();
|
|
119
|
+
canvas.requestRenderAll();
|
|
120
|
+
events.emit("load:success", { document });
|
|
121
|
+
return document;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const engineError = toLoadError(error, controller.signal);
|
|
124
|
+
events.emit("load:error", { error: engineError });
|
|
125
|
+
throw engineError;
|
|
126
|
+
} finally {
|
|
127
|
+
if (activeLoad === controller) activeLoad = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function requireStorage() {
|
|
131
|
+
if (!storage) throw new DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
|
|
132
|
+
return storage;
|
|
133
|
+
}
|
|
134
|
+
async function load(documentId, loadOptions) {
|
|
135
|
+
ensureUsable();
|
|
136
|
+
const source = requireStorage();
|
|
137
|
+
let stored;
|
|
138
|
+
try {
|
|
139
|
+
stored = await source.loadDocument(documentId);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
142
|
+
const engineError = new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
|
|
143
|
+
events.emit("load:error", { error: engineError });
|
|
144
|
+
throw engineError;
|
|
145
|
+
}
|
|
146
|
+
return loadDocument(stored, loadOptions);
|
|
147
|
+
}
|
|
148
|
+
async function save() {
|
|
149
|
+
ensureUsable();
|
|
150
|
+
const target = requireStorage();
|
|
151
|
+
const document = toDocument();
|
|
152
|
+
events.emit("save:start", { document });
|
|
153
|
+
try {
|
|
154
|
+
await target.saveDocument(document);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
157
|
+
const engineError = new DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, { cause: error });
|
|
158
|
+
events.emit("save:error", { error: engineError });
|
|
159
|
+
throw engineError;
|
|
160
|
+
}
|
|
161
|
+
events.emit("save:success", { document });
|
|
162
|
+
return document;
|
|
163
|
+
}
|
|
164
|
+
function newDocument(newOptions) {
|
|
165
|
+
ensureUsable();
|
|
166
|
+
activeLoad?.abort();
|
|
167
|
+
canvas.clear();
|
|
168
|
+
objectsById.clear();
|
|
169
|
+
documentInfo = createDocumentInfo(newOptions);
|
|
170
|
+
}
|
|
171
|
+
function getObjectById(id) {
|
|
172
|
+
ensureUsable();
|
|
173
|
+
const indexed = objectsById.get(id);
|
|
174
|
+
if (indexed !== void 0) return indexed;
|
|
175
|
+
rebuildIndex();
|
|
176
|
+
return objectsById.get(id);
|
|
177
|
+
}
|
|
178
|
+
function destroy() {
|
|
179
|
+
if (destroyed) return;
|
|
180
|
+
destroyed = true;
|
|
181
|
+
activeLoad?.abort();
|
|
182
|
+
canvas.off("object:added", handleObjectAdded);
|
|
183
|
+
canvas.off("object:removed", handleObjectRemoved);
|
|
184
|
+
objectsById.clear();
|
|
185
|
+
events.clear();
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
canvas,
|
|
189
|
+
getDocumentInfo: () => ({
|
|
190
|
+
...documentInfo,
|
|
191
|
+
metadata: { ...documentInfo.metadata }
|
|
192
|
+
}),
|
|
193
|
+
updateMetadata(changes) {
|
|
194
|
+
ensureUsable();
|
|
195
|
+
documentInfo = {
|
|
196
|
+
...documentInfo,
|
|
197
|
+
metadata: {
|
|
198
|
+
...documentInfo.metadata,
|
|
199
|
+
...changes
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
newDocument,
|
|
204
|
+
toDocument,
|
|
205
|
+
loadDocument,
|
|
206
|
+
load,
|
|
207
|
+
save,
|
|
208
|
+
registerObject(definition) {
|
|
209
|
+
ensureUsable();
|
|
210
|
+
registry.register(definition);
|
|
211
|
+
},
|
|
212
|
+
getObjectById,
|
|
213
|
+
on: (name, handler) => events.on(name, handler),
|
|
214
|
+
destroy
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { createDocumentEngine };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/engine/errors.ts
|
|
2
|
+
var DocumentEngineError = class extends Error {
|
|
3
|
+
constructor(code, message, details = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DocumentEngineError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.issues = details.issues ?? [];
|
|
8
|
+
this.unknownTypes = details.unknownTypes ?? [];
|
|
9
|
+
this.cause = details.cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function isDocumentEngineError(value) {
|
|
13
|
+
return value instanceof DocumentEngineError;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
exports.DocumentEngineError = DocumentEngineError;
|
|
17
|
+
exports.isDocumentEngineError = isDocumentEngineError;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/engine/errors.d.ts
|
|
2
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "ENGINE_DESTROYED";
|
|
3
|
+
export interface DocumentIssue {
|
|
4
|
+
code: DocumentErrorCode;
|
|
5
|
+
path: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DocumentEngineErrorDetails {
|
|
9
|
+
issues?: DocumentIssue[];
|
|
10
|
+
unknownTypes?: string[];
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare class DocumentEngineError extends Error {
|
|
14
|
+
readonly code: DocumentErrorCode;
|
|
15
|
+
readonly issues: DocumentIssue[];
|
|
16
|
+
readonly unknownTypes: string[];
|
|
17
|
+
readonly cause: unknown;
|
|
18
|
+
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
|
19
|
+
}
|
|
20
|
+
export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/engine/errors.d.ts
|
|
2
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "ENGINE_DESTROYED";
|
|
3
|
+
export interface DocumentIssue {
|
|
4
|
+
code: DocumentErrorCode;
|
|
5
|
+
path: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DocumentEngineErrorDetails {
|
|
9
|
+
issues?: DocumentIssue[];
|
|
10
|
+
unknownTypes?: string[];
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare class DocumentEngineError extends Error {
|
|
14
|
+
readonly code: DocumentErrorCode;
|
|
15
|
+
readonly issues: DocumentIssue[];
|
|
16
|
+
readonly unknownTypes: string[];
|
|
17
|
+
readonly cause: unknown;
|
|
18
|
+
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
|
19
|
+
}
|
|
20
|
+
export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/engine/errors.ts
|
|
2
|
+
var DocumentEngineError = class extends Error {
|
|
3
|
+
constructor(code, message, details = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DocumentEngineError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.issues = details.issues ?? [];
|
|
8
|
+
this.unknownTypes = details.unknownTypes ?? [];
|
|
9
|
+
this.cause = details.cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function isDocumentEngineError(value) {
|
|
13
|
+
return value instanceof DocumentEngineError;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DocumentEngineError, isDocumentEngineError };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/engine/event-emitter.ts
|
|
2
|
+
function createEventEmitter() {
|
|
3
|
+
const handlersByName = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
on(name, handler) {
|
|
6
|
+
const handlers = handlersByName.get(name) ?? /* @__PURE__ */ new Set();
|
|
7
|
+
handlers.add(handler);
|
|
8
|
+
handlersByName.set(name, handlers);
|
|
9
|
+
return () => {
|
|
10
|
+
handlers.delete(handler);
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
emit(name, payload) {
|
|
14
|
+
const handlers = handlersByName.get(name);
|
|
15
|
+
if (!handlers) return;
|
|
16
|
+
for (const handler of [...handlers]) handler(payload);
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
handlersByName.clear();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
exports.createEventEmitter = createEventEmitter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/engine/event-emitter.ts
|
|
2
|
+
function createEventEmitter() {
|
|
3
|
+
const handlersByName = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
on(name, handler) {
|
|
6
|
+
const handlers = handlersByName.get(name) ?? /* @__PURE__ */ new Set();
|
|
7
|
+
handlers.add(handler);
|
|
8
|
+
handlersByName.set(name, handlers);
|
|
9
|
+
return () => {
|
|
10
|
+
handlers.delete(handler);
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
emit(name, payload) {
|
|
14
|
+
const handlers = handlersByName.get(name);
|
|
15
|
+
if (!handlers) return;
|
|
16
|
+
for (const handler of [...handlers]) handler(payload);
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
handlersByName.clear();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { createEventEmitter };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const require_walk_objects = require("./walk-objects.cjs");
|
|
2
|
+
//#region src/fabric/fabric-adapter.ts
|
|
3
|
+
function keepOriginExplicit(liveObjects, serializedObjects) {
|
|
4
|
+
const pending = [];
|
|
5
|
+
serializedObjects.forEach((serialized, index) => {
|
|
6
|
+
const live = liveObjects[index];
|
|
7
|
+
if (live) pending.push([live, serialized]);
|
|
8
|
+
});
|
|
9
|
+
while (pending.length > 0) {
|
|
10
|
+
const [live, serialized] = pending.pop();
|
|
11
|
+
serialized.originX ?? (serialized.originX = live.originX);
|
|
12
|
+
serialized.originY ?? (serialized.originY = live.originY);
|
|
13
|
+
const liveChildren = require_walk_objects.childrenOf(live);
|
|
14
|
+
serialized.objects?.forEach((child, index) => {
|
|
15
|
+
const liveChild = liveChildren[index];
|
|
16
|
+
if (liveChild) pending.push([liveChild, child]);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function serializeCanvas(canvas, propertiesToInclude) {
|
|
21
|
+
const serialized = canvas.toObject(propertiesToInclude);
|
|
22
|
+
const objects = serialized.objects ?? [];
|
|
23
|
+
keepOriginExplicit(canvas.getObjects(), objects);
|
|
24
|
+
return {
|
|
25
|
+
version: serialized.version,
|
|
26
|
+
background: serialized.background,
|
|
27
|
+
objects
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function countSerializedTree(root) {
|
|
31
|
+
let count = 0;
|
|
32
|
+
const pending = [root];
|
|
33
|
+
while (pending.length > 0) {
|
|
34
|
+
const object = pending.pop();
|
|
35
|
+
count += 1;
|
|
36
|
+
if (object.objects) pending.push(...object.objects);
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
function countLiveTree(root) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
require_walk_objects.walkObjects([root], () => {
|
|
43
|
+
count += 1;
|
|
44
|
+
});
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
function describeObject(serialized) {
|
|
48
|
+
return serialized.id ? `"${serialized.type}" object ${serialized.id}` : `"${serialized.type}" object`;
|
|
49
|
+
}
|
|
50
|
+
function refuseMissingObjects(serialized, created, error) {
|
|
51
|
+
if (error !== void 0 || created === void 0) {
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error ?? "unknown reason");
|
|
53
|
+
throw new Error(`The ${describeObject(serialized)} could not be created: ${reason}`);
|
|
54
|
+
}
|
|
55
|
+
const expected = countSerializedTree(serialized);
|
|
56
|
+
const actual = countLiveTree(created);
|
|
57
|
+
if (actual !== expected) throw new Error(`Only ${actual} of ${expected} objects inside the ${describeObject(serialized)} could be created`);
|
|
58
|
+
}
|
|
59
|
+
async function loadIntoCanvas(canvas, content, signal) {
|
|
60
|
+
const json = { objects: content.objects };
|
|
61
|
+
if (content.version !== void 0) json.version = content.version;
|
|
62
|
+
if (content.background !== void 0) json.background = content.background;
|
|
63
|
+
const reviver = refuseMissingObjects;
|
|
64
|
+
await canvas.loadFromJSON(json, reviver, { signal });
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
exports.loadIntoCanvas = loadIntoCanvas;
|
|
68
|
+
exports.serializeCanvas = serializeCanvas;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { childrenOf, walkObjects } from "./walk-objects.js";
|
|
2
|
+
//#region src/fabric/fabric-adapter.ts
|
|
3
|
+
function keepOriginExplicit(liveObjects, serializedObjects) {
|
|
4
|
+
const pending = [];
|
|
5
|
+
serializedObjects.forEach((serialized, index) => {
|
|
6
|
+
const live = liveObjects[index];
|
|
7
|
+
if (live) pending.push([live, serialized]);
|
|
8
|
+
});
|
|
9
|
+
while (pending.length > 0) {
|
|
10
|
+
const [live, serialized] = pending.pop();
|
|
11
|
+
serialized.originX ?? (serialized.originX = live.originX);
|
|
12
|
+
serialized.originY ?? (serialized.originY = live.originY);
|
|
13
|
+
const liveChildren = childrenOf(live);
|
|
14
|
+
serialized.objects?.forEach((child, index) => {
|
|
15
|
+
const liveChild = liveChildren[index];
|
|
16
|
+
if (liveChild) pending.push([liveChild, child]);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function serializeCanvas(canvas, propertiesToInclude) {
|
|
21
|
+
const serialized = canvas.toObject(propertiesToInclude);
|
|
22
|
+
const objects = serialized.objects ?? [];
|
|
23
|
+
keepOriginExplicit(canvas.getObjects(), objects);
|
|
24
|
+
return {
|
|
25
|
+
version: serialized.version,
|
|
26
|
+
background: serialized.background,
|
|
27
|
+
objects
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function countSerializedTree(root) {
|
|
31
|
+
let count = 0;
|
|
32
|
+
const pending = [root];
|
|
33
|
+
while (pending.length > 0) {
|
|
34
|
+
const object = pending.pop();
|
|
35
|
+
count += 1;
|
|
36
|
+
if (object.objects) pending.push(...object.objects);
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
function countLiveTree(root) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
walkObjects([root], () => {
|
|
43
|
+
count += 1;
|
|
44
|
+
});
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
function describeObject(serialized) {
|
|
48
|
+
return serialized.id ? `"${serialized.type}" object ${serialized.id}` : `"${serialized.type}" object`;
|
|
49
|
+
}
|
|
50
|
+
function refuseMissingObjects(serialized, created, error) {
|
|
51
|
+
if (error !== void 0 || created === void 0) {
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error ?? "unknown reason");
|
|
53
|
+
throw new Error(`The ${describeObject(serialized)} could not be created: ${reason}`);
|
|
54
|
+
}
|
|
55
|
+
const expected = countSerializedTree(serialized);
|
|
56
|
+
const actual = countLiveTree(created);
|
|
57
|
+
if (actual !== expected) throw new Error(`Only ${actual} of ${expected} objects inside the ${describeObject(serialized)} could be created`);
|
|
58
|
+
}
|
|
59
|
+
async function loadIntoCanvas(canvas, content, signal) {
|
|
60
|
+
const json = { objects: content.objects };
|
|
61
|
+
if (content.version !== void 0) json.version = content.version;
|
|
62
|
+
if (content.background !== void 0) json.background = content.background;
|
|
63
|
+
const reviver = refuseMissingObjects;
|
|
64
|
+
await canvas.loadFromJSON(json, reviver, { signal });
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { loadIntoCanvas, serializeCanvas };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/fabric/object-ids.ts
|
|
2
|
+
function readObjectId(object) {
|
|
3
|
+
const id = object.id;
|
|
4
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
5
|
+
}
|
|
6
|
+
function writeObjectId(object, id) {
|
|
7
|
+
object.id = id;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
exports.readObjectId = readObjectId;
|
|
11
|
+
exports.writeObjectId = writeObjectId;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/fabric/object-ids.ts
|
|
2
|
+
function readObjectId(object) {
|
|
3
|
+
const id = object.id;
|
|
4
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
5
|
+
}
|
|
6
|
+
function writeObjectId(object, id) {
|
|
7
|
+
object.id = id;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { readObjectId, writeObjectId };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
let fabric = require("fabric");
|
|
3
|
+
//#region src/fabric/object-registry.ts
|
|
4
|
+
function createObjectRegistry(initialDefinitions = []) {
|
|
5
|
+
const extraProperties = /* @__PURE__ */ new Set(["id"]);
|
|
6
|
+
function register(definition) {
|
|
7
|
+
const { fabricClass } = definition;
|
|
8
|
+
if (typeof fabricClass?.type !== "string" || fabricClass.type.length === 0) throw new require_errors.DocumentEngineError("INVALID_CUSTOM_OBJECT", "A custom object class needs a static \"type\" string, for example: static type = \"Sticker\"");
|
|
9
|
+
if (typeof fabricClass.fromObject !== "function") throw new require_errors.DocumentEngineError("INVALID_CUSTOM_OBJECT", `Custom object "${fabricClass.type}" must extend a Fabric class so it has a static fromObject method`);
|
|
10
|
+
fabric.classRegistry.setClass(fabricClass);
|
|
11
|
+
for (const property of definition.properties ?? []) extraProperties.add(property);
|
|
12
|
+
}
|
|
13
|
+
initialDefinitions.forEach(register);
|
|
14
|
+
return {
|
|
15
|
+
register,
|
|
16
|
+
propertiesToInclude: () => [...extraProperties],
|
|
17
|
+
findUnknownTypes: (types) => [...types].filter((type) => !fabric.classRegistry.has(type))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
exports.createObjectRegistry = createObjectRegistry;
|