fabricjs-document-engine 1.0.2 → 1.1.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/README.md +181 -10
- package/README.zh-CN.md +181 -10
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +440 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- package/package.json +30 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SerializedFabricObject } from "../document/document-format.cjs";
|
|
2
|
+
import { ContentLimits } from "../security/content-limits.cjs";
|
|
3
|
+
import { DocumentEngine } from "../engine/create-document-engine.cjs";
|
|
4
|
+
import { FabricObject } from "fabric";
|
|
5
|
+
//#region src/commands/clipboard.d.ts
|
|
6
|
+
export declare const CLIPBOARD_FORMAT = "fabricjs-document-engine/objects";
|
|
7
|
+
/** What the clipboard holds. Plain JSON, so an app can put it on the system clipboard. */
|
|
8
|
+
export interface ClipboardContent {
|
|
9
|
+
format: typeof CLIPBOARD_FORMAT;
|
|
10
|
+
version: 1;
|
|
11
|
+
objects: SerializedFabricObject[];
|
|
12
|
+
}
|
|
13
|
+
export interface ClipboardOptions {
|
|
14
|
+
/** How far each paste moves from the last one, in canvas units. Default 10. */
|
|
15
|
+
offset?: number;
|
|
16
|
+
/** Limits for content passed to `write`, as for loading documents. */
|
|
17
|
+
limits?: ContentLimits;
|
|
18
|
+
}
|
|
19
|
+
export interface PasteOptions {
|
|
20
|
+
/** The engine to paste into. Defaults to the engine the clipboard was made for. */
|
|
21
|
+
target?: DocumentEngine;
|
|
22
|
+
/** Overrides the offset for this paste. */
|
|
23
|
+
offset?: number;
|
|
24
|
+
/** Selects the pasted objects on an interactive canvas. Default true. */
|
|
25
|
+
select?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface Clipboard {
|
|
28
|
+
/** Copies objects, or the current selection. Returns how many objects were copied. */
|
|
29
|
+
copy(objects?: readonly FabricObject[]): number;
|
|
30
|
+
/** Copies and then removes objects, as one undo step. Returns how many objects were cut. */
|
|
31
|
+
cut(objects?: readonly FabricObject[]): number;
|
|
32
|
+
/** Adds a copy of the clipboard with fresh ids, as one undo step. Returns the new objects. */
|
|
33
|
+
paste(options?: PasteOptions): Promise<FabricObject[]>;
|
|
34
|
+
hasContent(): boolean;
|
|
35
|
+
/** The copied objects as JSON, or `undefined` when the clipboard is empty. */
|
|
36
|
+
read(): ClipboardContent | undefined;
|
|
37
|
+
/** Replaces the clipboard with content from `read`, for example from another tab. */
|
|
38
|
+
write(content: unknown): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A clipboard for one engine. Copies keep custom properties and the exact
|
|
43
|
+
* position of objects in groups and selections; pastes get fresh ids for
|
|
44
|
+
* every object and child and count as one undo step.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createClipboard(engine: DocumentEngine, options?: ClipboardOptions): Clipboard;
|
|
47
|
+
//#endregion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SerializedFabricObject } from "../document/document-format.js";
|
|
2
|
+
import { ContentLimits } from "../security/content-limits.js";
|
|
3
|
+
import { DocumentEngine } from "../engine/create-document-engine.js";
|
|
4
|
+
import { FabricObject } from "fabric";
|
|
5
|
+
//#region src/commands/clipboard.d.ts
|
|
6
|
+
export declare const CLIPBOARD_FORMAT = "fabricjs-document-engine/objects";
|
|
7
|
+
/** What the clipboard holds. Plain JSON, so an app can put it on the system clipboard. */
|
|
8
|
+
export interface ClipboardContent {
|
|
9
|
+
format: typeof CLIPBOARD_FORMAT;
|
|
10
|
+
version: 1;
|
|
11
|
+
objects: SerializedFabricObject[];
|
|
12
|
+
}
|
|
13
|
+
export interface ClipboardOptions {
|
|
14
|
+
/** How far each paste moves from the last one, in canvas units. Default 10. */
|
|
15
|
+
offset?: number;
|
|
16
|
+
/** Limits for content passed to `write`, as for loading documents. */
|
|
17
|
+
limits?: ContentLimits;
|
|
18
|
+
}
|
|
19
|
+
export interface PasteOptions {
|
|
20
|
+
/** The engine to paste into. Defaults to the engine the clipboard was made for. */
|
|
21
|
+
target?: DocumentEngine;
|
|
22
|
+
/** Overrides the offset for this paste. */
|
|
23
|
+
offset?: number;
|
|
24
|
+
/** Selects the pasted objects on an interactive canvas. Default true. */
|
|
25
|
+
select?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface Clipboard {
|
|
28
|
+
/** Copies objects, or the current selection. Returns how many objects were copied. */
|
|
29
|
+
copy(objects?: readonly FabricObject[]): number;
|
|
30
|
+
/** Copies and then removes objects, as one undo step. Returns how many objects were cut. */
|
|
31
|
+
cut(objects?: readonly FabricObject[]): number;
|
|
32
|
+
/** Adds a copy of the clipboard with fresh ids, as one undo step. Returns the new objects. */
|
|
33
|
+
paste(options?: PasteOptions): Promise<FabricObject[]>;
|
|
34
|
+
hasContent(): boolean;
|
|
35
|
+
/** The copied objects as JSON, or `undefined` when the clipboard is empty. */
|
|
36
|
+
read(): ClipboardContent | undefined;
|
|
37
|
+
/** Replaces the clipboard with content from `read`, for example from another tab. */
|
|
38
|
+
write(content: unknown): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A clipboard for one engine. Copies keep custom properties and the exact
|
|
43
|
+
* position of objects in groups and selections; pastes get fresh ids for
|
|
44
|
+
* every object and child and count as one undo step.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createClipboard(engine: DocumentEngine, options?: ClipboardOptions): Clipboard;
|
|
47
|
+
//#endregion
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import { refuseUnsafeImageUrls, secureDocument } from "../security/content-limits.js";
|
|
3
|
+
import { readObjectId } from "../fabric/object-ids.js";
|
|
4
|
+
import { collectSerializedTypes } from "../fabric/walk-objects.js";
|
|
5
|
+
import { createObjects } from "../fabric/fabric-adapter.js";
|
|
6
|
+
import { ActiveSelection, classRegistry } from "fabric";
|
|
7
|
+
//#region src/commands/clipboard.ts
|
|
8
|
+
const CLIPBOARD_FORMAT = "fabricjs-document-engine/objects";
|
|
9
|
+
function clone(value) {
|
|
10
|
+
return JSON.parse(JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
/** Removes every id, so the engine gives each pasted object, child and clip path a new one. */
|
|
13
|
+
function withoutIds(object) {
|
|
14
|
+
const copy = { ...object };
|
|
15
|
+
delete copy.id;
|
|
16
|
+
if (Array.isArray(copy.objects)) copy.objects = copy.objects.map(withoutIds);
|
|
17
|
+
if (copy.clipPath && typeof copy.clipPath === "object") copy.clipPath = withoutIds(copy.clipPath);
|
|
18
|
+
return copy;
|
|
19
|
+
}
|
|
20
|
+
function moved(object, distance) {
|
|
21
|
+
const left = typeof object.left === "number" ? object.left : 0;
|
|
22
|
+
const top = typeof object.top === "number" ? object.top : 0;
|
|
23
|
+
return {
|
|
24
|
+
...object,
|
|
25
|
+
left: left + distance,
|
|
26
|
+
top: top + distance
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function refuseInvalidContent(message) {
|
|
30
|
+
throw new DocumentEngineError("INVALID_DOCUMENT", `The clipboard content is not valid: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
function checkContent(content, limits) {
|
|
33
|
+
if (typeof content !== "object" || content === null) refuseInvalidContent("it is not an object");
|
|
34
|
+
const candidate = content;
|
|
35
|
+
if (candidate.format !== "fabricjs-document-engine/objects") refuseInvalidContent(`its format is not "${CLIPBOARD_FORMAT}"`);
|
|
36
|
+
if (candidate.version !== 1) refuseInvalidContent("its version is not 1");
|
|
37
|
+
if (!Array.isArray(candidate.objects)) refuseInvalidContent("it has no objects list");
|
|
38
|
+
const secured = secureDocument({ objects: candidate.objects }, limits);
|
|
39
|
+
const objects = secured.objects.filter((object) => typeof object === "object" && object !== null && typeof object.type === "string");
|
|
40
|
+
if (objects.length !== secured.objects.length) refuseInvalidContent("every object needs a type");
|
|
41
|
+
refuseUnsafeImageUrls(objects, limits?.isAllowedUrl);
|
|
42
|
+
return {
|
|
43
|
+
format: CLIPBOARD_FORMAT,
|
|
44
|
+
version: 1,
|
|
45
|
+
objects
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function topLevelInOrder(engine, objects) {
|
|
49
|
+
const stack = engine.canvas.getObjects();
|
|
50
|
+
const wanted = new Set(objects);
|
|
51
|
+
return stack.filter((object) => wanted.has(object));
|
|
52
|
+
}
|
|
53
|
+
function selectionOf(engine) {
|
|
54
|
+
return engine.canvas.getActiveObjects?.() ?? [];
|
|
55
|
+
}
|
|
56
|
+
function refuseUnknownTypes(objects) {
|
|
57
|
+
const unknownTypes = [...collectSerializedTypes(objects)].filter((type) => !classRegistry.has(type));
|
|
58
|
+
if (unknownTypes.length === 0) return;
|
|
59
|
+
throw new DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The clipboard holds object types that are not registered: ${unknownTypes.join(", ")}`, { unknownTypes });
|
|
60
|
+
}
|
|
61
|
+
function select(engine, objects) {
|
|
62
|
+
const canvas = engine.canvas;
|
|
63
|
+
if (typeof canvas.setActiveObject !== "function" || objects.length === 0) return;
|
|
64
|
+
canvas.discardActiveObject?.();
|
|
65
|
+
canvas.setActiveObject(objects.length === 1 ? objects[0] : new ActiveSelection(objects, { canvas: engine.canvas }));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A clipboard for one engine. Copies keep custom properties and the exact
|
|
69
|
+
* position of objects in groups and selections; pastes get fresh ids for
|
|
70
|
+
* every object and child and count as one undo step.
|
|
71
|
+
*/
|
|
72
|
+
function createClipboard(engine, options = {}) {
|
|
73
|
+
const defaultOffset = options.offset ?? 10;
|
|
74
|
+
let content;
|
|
75
|
+
let pastes = 0;
|
|
76
|
+
function copyObjects(objects) {
|
|
77
|
+
const chosen = topLevelInOrder(engine, objects);
|
|
78
|
+
if (chosen.length === 0) return 0;
|
|
79
|
+
const document = engine.toDocument();
|
|
80
|
+
const serialized = chosen.map((object) => readObjectId(object)).map((id) => document.objects.find((object) => object.id === id)).filter((object) => object !== void 0);
|
|
81
|
+
content = {
|
|
82
|
+
format: CLIPBOARD_FORMAT,
|
|
83
|
+
version: 1,
|
|
84
|
+
objects: clone(serialized)
|
|
85
|
+
};
|
|
86
|
+
return serialized.length;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
copy(objects) {
|
|
90
|
+
const count = copyObjects(objects ?? selectionOf(engine));
|
|
91
|
+
if (count > 0) pastes = 0;
|
|
92
|
+
return count;
|
|
93
|
+
},
|
|
94
|
+
cut(objects) {
|
|
95
|
+
const chosen = topLevelInOrder(engine, objects ?? selectionOf(engine));
|
|
96
|
+
const count = copyObjects(chosen);
|
|
97
|
+
if (count === 0) return 0;
|
|
98
|
+
pastes = -1;
|
|
99
|
+
engine.transaction(count === 1 ? "Cut" : `Cut ${count} objects`, () => {
|
|
100
|
+
engine.canvas.discardActiveObject?.();
|
|
101
|
+
engine.canvas.remove(...chosen);
|
|
102
|
+
});
|
|
103
|
+
engine.canvas.requestRenderAll();
|
|
104
|
+
return count;
|
|
105
|
+
},
|
|
106
|
+
async paste(pasteOptions = {}) {
|
|
107
|
+
if (!content) return [];
|
|
108
|
+
const target = pasteOptions.target ?? engine;
|
|
109
|
+
const offset = pasteOptions.offset ?? defaultOffset;
|
|
110
|
+
pastes += 1;
|
|
111
|
+
const distance = offset * pastes;
|
|
112
|
+
const serialized = content.objects.map((object) => moved(withoutIds(object), distance));
|
|
113
|
+
refuseUnknownTypes(serialized);
|
|
114
|
+
const created = await createObjects(clone(serialized));
|
|
115
|
+
target.transaction(created.length === 1 ? "Paste" : `Paste ${created.length} objects`, () => {
|
|
116
|
+
target.canvas.add(...created);
|
|
117
|
+
});
|
|
118
|
+
if (pasteOptions.select ?? true) select(target, created);
|
|
119
|
+
target.canvas.requestRenderAll();
|
|
120
|
+
return created;
|
|
121
|
+
},
|
|
122
|
+
hasContent: () => content !== void 0,
|
|
123
|
+
read: () => content ? clone(content) : void 0,
|
|
124
|
+
write(next) {
|
|
125
|
+
content = checkContent(next, options.limits);
|
|
126
|
+
pastes = 0;
|
|
127
|
+
},
|
|
128
|
+
clear() {
|
|
129
|
+
content = void 0;
|
|
130
|
+
pastes = 0;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { CLIPBOARD_FORMAT, createClipboard };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
2
|
+
//#region src/commands/layers.ts
|
|
3
|
+
function chosenObjects(engine, objects) {
|
|
4
|
+
const list = objects ?? engine.canvas.getActiveObjects?.() ?? [];
|
|
5
|
+
const stack = new Set(engine.canvas.getObjects());
|
|
6
|
+
return new Set(list.filter((object) => stack.has(object)));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Works out the new order on the objects that may move, then puts every
|
|
10
|
+
* pinned object back at the index it had.
|
|
11
|
+
*/
|
|
12
|
+
function reorder(engine, label, objects, options, arrange) {
|
|
13
|
+
const stack = engine.canvas.getObjects();
|
|
14
|
+
const isPinned = options.pinned ?? (() => false);
|
|
15
|
+
const chosen = chosenObjects(engine, objects);
|
|
16
|
+
for (const object of chosen) if (isPinned(object)) chosen.delete(object);
|
|
17
|
+
if (chosen.size === 0) return false;
|
|
18
|
+
const pinnedAt = /* @__PURE__ */ new Map();
|
|
19
|
+
const free = [];
|
|
20
|
+
stack.forEach((object, index) => {
|
|
21
|
+
if (isPinned(object)) pinnedAt.set(index, object);
|
|
22
|
+
else free.push(object);
|
|
23
|
+
});
|
|
24
|
+
const arranged = arrange(free, chosen);
|
|
25
|
+
const next = [];
|
|
26
|
+
let freeIndex = 0;
|
|
27
|
+
for (let index = 0; index < stack.length; index += 1) next.push(pinnedAt.get(index) ?? arranged[freeIndex++]);
|
|
28
|
+
if (next.every((object, index) => object === stack[index])) return false;
|
|
29
|
+
engine.transaction(chosen.size === 1 ? label : `${label} (${chosen.size} objects)`, () => {
|
|
30
|
+
next.forEach((object, index) => {
|
|
31
|
+
if (engine.canvas.getObjects()[index] !== object) engine.canvas.moveObjectTo(object, index);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
engine.canvas.requestRenderAll();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** Moves objects, or the selection, to the top of the stack. Returns false when nothing moved. */
|
|
38
|
+
function bringToFront(engine, objects, options = {}) {
|
|
39
|
+
return reorder(engine, "Bring to front", objects, options, (free, chosen) => [...free.filter((object) => !chosen.has(object)), ...free.filter((object) => chosen.has(object))]);
|
|
40
|
+
}
|
|
41
|
+
/** Moves objects, or the selection, to the bottom of the stack. Returns false when nothing moved. */
|
|
42
|
+
function sendToBack(engine, objects, options = {}) {
|
|
43
|
+
return reorder(engine, "Send to back", objects, options, (free, chosen) => [...free.filter((object) => chosen.has(object)), ...free.filter((object) => !chosen.has(object))]);
|
|
44
|
+
}
|
|
45
|
+
/** Moves objects, or the selection, one step up. Selected objects next to each other move as a block. */
|
|
46
|
+
function bringForward(engine, objects, options = {}) {
|
|
47
|
+
return reorder(engine, "Bring forward", objects, options, (free, chosen) => {
|
|
48
|
+
const next = [...free];
|
|
49
|
+
for (let index = next.length - 2; index >= 0; index -= 1) if (chosen.has(next[index]) && !chosen.has(next[index + 1])) [next[index], next[index + 1]] = [next[index + 1], next[index]];
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Moves objects, or the selection, one step down. Selected objects next to each other move as a block. */
|
|
54
|
+
function sendBackward(engine, objects, options = {}) {
|
|
55
|
+
return reorder(engine, "Send backward", objects, options, (free, chosen) => {
|
|
56
|
+
const next = [...free];
|
|
57
|
+
for (let index = 1; index < next.length; index += 1) if (chosen.has(next[index]) && !chosen.has(next[index - 1])) [next[index], next[index - 1]] = [next[index - 1], next[index]];
|
|
58
|
+
return next;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Moves objects to `index`, counted from the bottom among objects that are not
|
|
63
|
+
* pinned. Several objects keep their order and land as a block.
|
|
64
|
+
*/
|
|
65
|
+
function moveToIndex(engine, objects, index, options = {}) {
|
|
66
|
+
return reorder(engine, "Move layer", objects, options, (free, chosen) => {
|
|
67
|
+
const rest = free.filter((object) => !chosen.has(object));
|
|
68
|
+
const block = free.filter((object) => chosen.has(object));
|
|
69
|
+
const at = Math.max(0, Math.min(Math.floor(index), rest.length));
|
|
70
|
+
return [
|
|
71
|
+
...rest.slice(0, at),
|
|
72
|
+
...block,
|
|
73
|
+
...rest.slice(at)
|
|
74
|
+
];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** The top-level objects from top to bottom, for a layers panel. */
|
|
78
|
+
function getLayers(engine) {
|
|
79
|
+
return engine.canvas.getObjects().map((object, index) => {
|
|
80
|
+
const name = object.name;
|
|
81
|
+
return {
|
|
82
|
+
id: require_object_ids.readObjectId(object),
|
|
83
|
+
type: object.type,
|
|
84
|
+
name: typeof name === "string" ? name : void 0,
|
|
85
|
+
index,
|
|
86
|
+
visible: object.visible !== false,
|
|
87
|
+
locked: object.selectable === false
|
|
88
|
+
};
|
|
89
|
+
}).reverse();
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
exports.bringForward = bringForward;
|
|
93
|
+
exports.bringToFront = bringToFront;
|
|
94
|
+
exports.getLayers = getLayers;
|
|
95
|
+
exports.moveToIndex = moveToIndex;
|
|
96
|
+
exports.sendBackward = sendBackward;
|
|
97
|
+
exports.sendToBack = sendToBack;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DocumentEngine } from "../engine/create-document-engine.cjs";
|
|
2
|
+
import { FabricObject } from "fabric";
|
|
3
|
+
//#region src/commands/layers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Keeps an object where it is in the stack. Use it for a background that must
|
|
6
|
+
* stay at the bottom or a frame that must stay on top.
|
|
7
|
+
*/
|
|
8
|
+
export type LayerPin = (object: FabricObject) => boolean;
|
|
9
|
+
export interface LayerOptions {
|
|
10
|
+
/** Objects this returns true for never move, and keep their place in the stack. */
|
|
11
|
+
pinned?: LayerPin;
|
|
12
|
+
}
|
|
13
|
+
export interface LayerInfo {
|
|
14
|
+
id: string | undefined;
|
|
15
|
+
type: string;
|
|
16
|
+
/** The object's `name` property, when it has one. */
|
|
17
|
+
name: string | undefined;
|
|
18
|
+
/** Position from the bottom of the stack, as Fabric counts it. */
|
|
19
|
+
index: number;
|
|
20
|
+
visible: boolean;
|
|
21
|
+
/** True when the object cannot be selected. */
|
|
22
|
+
locked: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Moves objects, or the selection, to the top of the stack. Returns false when nothing moved. */
|
|
25
|
+
export declare function bringToFront(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
26
|
+
/** Moves objects, or the selection, to the bottom of the stack. Returns false when nothing moved. */
|
|
27
|
+
export declare function sendToBack(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
28
|
+
/** Moves objects, or the selection, one step up. Selected objects next to each other move as a block. */
|
|
29
|
+
export declare function bringForward(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
30
|
+
/** Moves objects, or the selection, one step down. Selected objects next to each other move as a block. */
|
|
31
|
+
export declare function sendBackward(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Moves objects to `index`, counted from the bottom among objects that are not
|
|
34
|
+
* pinned. Several objects keep their order and land as a block.
|
|
35
|
+
*/
|
|
36
|
+
export declare function moveToIndex(engine: DocumentEngine, objects: readonly FabricObject[], index: number, options?: LayerOptions): boolean;
|
|
37
|
+
/** The top-level objects from top to bottom, for a layers panel. */
|
|
38
|
+
export declare function getLayers(engine: DocumentEngine): LayerInfo[];
|
|
39
|
+
//#endregion
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DocumentEngine } from "../engine/create-document-engine.js";
|
|
2
|
+
import { FabricObject } from "fabric";
|
|
3
|
+
//#region src/commands/layers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Keeps an object where it is in the stack. Use it for a background that must
|
|
6
|
+
* stay at the bottom or a frame that must stay on top.
|
|
7
|
+
*/
|
|
8
|
+
export type LayerPin = (object: FabricObject) => boolean;
|
|
9
|
+
export interface LayerOptions {
|
|
10
|
+
/** Objects this returns true for never move, and keep their place in the stack. */
|
|
11
|
+
pinned?: LayerPin;
|
|
12
|
+
}
|
|
13
|
+
export interface LayerInfo {
|
|
14
|
+
id: string | undefined;
|
|
15
|
+
type: string;
|
|
16
|
+
/** The object's `name` property, when it has one. */
|
|
17
|
+
name: string | undefined;
|
|
18
|
+
/** Position from the bottom of the stack, as Fabric counts it. */
|
|
19
|
+
index: number;
|
|
20
|
+
visible: boolean;
|
|
21
|
+
/** True when the object cannot be selected. */
|
|
22
|
+
locked: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Moves objects, or the selection, to the top of the stack. Returns false when nothing moved. */
|
|
25
|
+
export declare function bringToFront(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
26
|
+
/** Moves objects, or the selection, to the bottom of the stack. Returns false when nothing moved. */
|
|
27
|
+
export declare function sendToBack(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
28
|
+
/** Moves objects, or the selection, one step up. Selected objects next to each other move as a block. */
|
|
29
|
+
export declare function bringForward(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
30
|
+
/** Moves objects, or the selection, one step down. Selected objects next to each other move as a block. */
|
|
31
|
+
export declare function sendBackward(engine: DocumentEngine, objects?: readonly FabricObject[], options?: LayerOptions): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Moves objects to `index`, counted from the bottom among objects that are not
|
|
34
|
+
* pinned. Several objects keep their order and land as a block.
|
|
35
|
+
*/
|
|
36
|
+
export declare function moveToIndex(engine: DocumentEngine, objects: readonly FabricObject[], index: number, options?: LayerOptions): boolean;
|
|
37
|
+
/** The top-level objects from top to bottom, for a layers panel. */
|
|
38
|
+
export declare function getLayers(engine: DocumentEngine): LayerInfo[];
|
|
39
|
+
//#endregion
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readObjectId } from "../fabric/object-ids.js";
|
|
2
|
+
//#region src/commands/layers.ts
|
|
3
|
+
function chosenObjects(engine, objects) {
|
|
4
|
+
const list = objects ?? engine.canvas.getActiveObjects?.() ?? [];
|
|
5
|
+
const stack = new Set(engine.canvas.getObjects());
|
|
6
|
+
return new Set(list.filter((object) => stack.has(object)));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Works out the new order on the objects that may move, then puts every
|
|
10
|
+
* pinned object back at the index it had.
|
|
11
|
+
*/
|
|
12
|
+
function reorder(engine, label, objects, options, arrange) {
|
|
13
|
+
const stack = engine.canvas.getObjects();
|
|
14
|
+
const isPinned = options.pinned ?? (() => false);
|
|
15
|
+
const chosen = chosenObjects(engine, objects);
|
|
16
|
+
for (const object of chosen) if (isPinned(object)) chosen.delete(object);
|
|
17
|
+
if (chosen.size === 0) return false;
|
|
18
|
+
const pinnedAt = /* @__PURE__ */ new Map();
|
|
19
|
+
const free = [];
|
|
20
|
+
stack.forEach((object, index) => {
|
|
21
|
+
if (isPinned(object)) pinnedAt.set(index, object);
|
|
22
|
+
else free.push(object);
|
|
23
|
+
});
|
|
24
|
+
const arranged = arrange(free, chosen);
|
|
25
|
+
const next = [];
|
|
26
|
+
let freeIndex = 0;
|
|
27
|
+
for (let index = 0; index < stack.length; index += 1) next.push(pinnedAt.get(index) ?? arranged[freeIndex++]);
|
|
28
|
+
if (next.every((object, index) => object === stack[index])) return false;
|
|
29
|
+
engine.transaction(chosen.size === 1 ? label : `${label} (${chosen.size} objects)`, () => {
|
|
30
|
+
next.forEach((object, index) => {
|
|
31
|
+
if (engine.canvas.getObjects()[index] !== object) engine.canvas.moveObjectTo(object, index);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
engine.canvas.requestRenderAll();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** Moves objects, or the selection, to the top of the stack. Returns false when nothing moved. */
|
|
38
|
+
function bringToFront(engine, objects, options = {}) {
|
|
39
|
+
return reorder(engine, "Bring to front", objects, options, (free, chosen) => [...free.filter((object) => !chosen.has(object)), ...free.filter((object) => chosen.has(object))]);
|
|
40
|
+
}
|
|
41
|
+
/** Moves objects, or the selection, to the bottom of the stack. Returns false when nothing moved. */
|
|
42
|
+
function sendToBack(engine, objects, options = {}) {
|
|
43
|
+
return reorder(engine, "Send to back", objects, options, (free, chosen) => [...free.filter((object) => chosen.has(object)), ...free.filter((object) => !chosen.has(object))]);
|
|
44
|
+
}
|
|
45
|
+
/** Moves objects, or the selection, one step up. Selected objects next to each other move as a block. */
|
|
46
|
+
function bringForward(engine, objects, options = {}) {
|
|
47
|
+
return reorder(engine, "Bring forward", objects, options, (free, chosen) => {
|
|
48
|
+
const next = [...free];
|
|
49
|
+
for (let index = next.length - 2; index >= 0; index -= 1) if (chosen.has(next[index]) && !chosen.has(next[index + 1])) [next[index], next[index + 1]] = [next[index + 1], next[index]];
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Moves objects, or the selection, one step down. Selected objects next to each other move as a block. */
|
|
54
|
+
function sendBackward(engine, objects, options = {}) {
|
|
55
|
+
return reorder(engine, "Send backward", objects, options, (free, chosen) => {
|
|
56
|
+
const next = [...free];
|
|
57
|
+
for (let index = 1; index < next.length; index += 1) if (chosen.has(next[index]) && !chosen.has(next[index - 1])) [next[index], next[index - 1]] = [next[index - 1], next[index]];
|
|
58
|
+
return next;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Moves objects to `index`, counted from the bottom among objects that are not
|
|
63
|
+
* pinned. Several objects keep their order and land as a block.
|
|
64
|
+
*/
|
|
65
|
+
function moveToIndex(engine, objects, index, options = {}) {
|
|
66
|
+
return reorder(engine, "Move layer", objects, options, (free, chosen) => {
|
|
67
|
+
const rest = free.filter((object) => !chosen.has(object));
|
|
68
|
+
const block = free.filter((object) => chosen.has(object));
|
|
69
|
+
const at = Math.max(0, Math.min(Math.floor(index), rest.length));
|
|
70
|
+
return [
|
|
71
|
+
...rest.slice(0, at),
|
|
72
|
+
...block,
|
|
73
|
+
...rest.slice(at)
|
|
74
|
+
];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** The top-level objects from top to bottom, for a layers panel. */
|
|
78
|
+
function getLayers(engine) {
|
|
79
|
+
return engine.canvas.getObjects().map((object, index) => {
|
|
80
|
+
const name = object.name;
|
|
81
|
+
return {
|
|
82
|
+
id: readObjectId(object),
|
|
83
|
+
type: object.type,
|
|
84
|
+
name: typeof name === "string" ? name : void 0,
|
|
85
|
+
index,
|
|
86
|
+
visible: object.visible !== false,
|
|
87
|
+
locked: object.selectable === false
|
|
88
|
+
};
|
|
89
|
+
}).reverse();
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { bringForward, bringToFront, getLayers, moveToIndex, sendBackward, sendToBack };
|