@overtone-art/canvas-editor-core 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +214 -1
- package/dist/index.d.ts +214 -1
- package/dist/index.js +660 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +646 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -11
package/dist/index.js
CHANGED
|
@@ -31,13 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
CanvasEditor: () => CanvasEditor,
|
|
34
|
+
CropController: () => CropController,
|
|
35
|
+
DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
|
|
34
36
|
EventEmitter: () => EventEmitter,
|
|
35
37
|
HistoryManager: () => HistoryManager,
|
|
36
38
|
Layer: () => Layer,
|
|
37
39
|
LayerManager: () => LayerManager,
|
|
40
|
+
PatternManager: () => PatternManager,
|
|
41
|
+
SnapManager: () => SnapManager,
|
|
38
42
|
UnitConverter: () => UnitConverter,
|
|
43
|
+
buildPatternDataURL: () => buildPatternDataURL,
|
|
39
44
|
clamp: () => clamp,
|
|
45
|
+
clearPatternImageCache: () => clearPatternImageCache,
|
|
46
|
+
computeTilePositions: () => computeTilePositions,
|
|
40
47
|
deserializeEditor: () => deserializeEditor,
|
|
48
|
+
drawTiles: () => drawTiles,
|
|
41
49
|
exportDataURL: () => exportDataURL,
|
|
42
50
|
exportPNG: () => exportPNG,
|
|
43
51
|
exportSVG: () => exportSVG,
|
|
@@ -48,7 +56,7 @@ __export(index_exports, {
|
|
|
48
56
|
module.exports = __toCommonJS(index_exports);
|
|
49
57
|
|
|
50
58
|
// src/editor.ts
|
|
51
|
-
var
|
|
59
|
+
var import_fabric3 = require("fabric");
|
|
52
60
|
|
|
53
61
|
// src/events.ts
|
|
54
62
|
var EventEmitter = class {
|
|
@@ -97,6 +105,8 @@ var Layer = class {
|
|
|
97
105
|
visible;
|
|
98
106
|
locked;
|
|
99
107
|
opacity;
|
|
108
|
+
/** Non-fabric data (e.g. pattern config) that must persist with the layer. */
|
|
109
|
+
meta;
|
|
100
110
|
fabricObject;
|
|
101
111
|
constructor(type, fabricObject, name, id) {
|
|
102
112
|
this.id = id ?? generateId();
|
|
@@ -105,9 +115,13 @@ var Layer = class {
|
|
|
105
115
|
this.visible = true;
|
|
106
116
|
this.locked = false;
|
|
107
117
|
this.opacity = 1;
|
|
118
|
+
this.meta = {};
|
|
108
119
|
this.fabricObject = fabricObject;
|
|
109
120
|
this.fabricObject._layerId = this.id;
|
|
110
121
|
}
|
|
122
|
+
hasMeta() {
|
|
123
|
+
return Object.keys(this.meta).length > 0;
|
|
124
|
+
}
|
|
111
125
|
toData() {
|
|
112
126
|
return {
|
|
113
127
|
id: this.id,
|
|
@@ -115,7 +129,8 @@ var Layer = class {
|
|
|
115
129
|
name: this.name,
|
|
116
130
|
visible: this.visible,
|
|
117
131
|
locked: this.locked,
|
|
118
|
-
opacity: this.opacity
|
|
132
|
+
opacity: this.opacity,
|
|
133
|
+
...this.hasMeta() ? { meta: this.meta } : {}
|
|
119
134
|
};
|
|
120
135
|
}
|
|
121
136
|
serialize() {
|
|
@@ -175,6 +190,8 @@ var LayerManager = class {
|
|
|
175
190
|
}
|
|
176
191
|
this.canvas.requestRenderAll();
|
|
177
192
|
this.events.emit("layer:selected", { layerId: id });
|
|
193
|
+
const selected = id && this.get(id) ? [id] : [];
|
|
194
|
+
this.events.emit("selection:changed", { selected });
|
|
178
195
|
}
|
|
179
196
|
get(id) {
|
|
180
197
|
return this.layers.find((l) => l.id === id);
|
|
@@ -273,6 +290,7 @@ var HistoryManager = class {
|
|
|
273
290
|
this.emitChanged();
|
|
274
291
|
}
|
|
275
292
|
async undo() {
|
|
293
|
+
this.cancelPending();
|
|
276
294
|
const state = this.undoStack.pop();
|
|
277
295
|
if (!state) return;
|
|
278
296
|
this.redoStack.push(this.getState());
|
|
@@ -282,6 +300,7 @@ var HistoryManager = class {
|
|
|
282
300
|
this.emitChanged();
|
|
283
301
|
}
|
|
284
302
|
async redo() {
|
|
303
|
+
this.cancelPending();
|
|
285
304
|
const state = this.redoStack.pop();
|
|
286
305
|
if (!state) return;
|
|
287
306
|
this.undoStack.push(this.getState());
|
|
@@ -290,6 +309,11 @@ var HistoryManager = class {
|
|
|
290
309
|
this.paused = false;
|
|
291
310
|
this.emitChanged();
|
|
292
311
|
}
|
|
312
|
+
/** True while a restore (undo/redo/deserialize) is in flight. Managers that
|
|
313
|
+
* react to canvas events should skip mutating history during this window. */
|
|
314
|
+
isRestoring() {
|
|
315
|
+
return this.paused;
|
|
316
|
+
}
|
|
293
317
|
pause() {
|
|
294
318
|
this.paused = true;
|
|
295
319
|
}
|
|
@@ -307,6 +331,12 @@ var HistoryManager = class {
|
|
|
307
331
|
this.redoStack = [];
|
|
308
332
|
this.emitChanged();
|
|
309
333
|
}
|
|
334
|
+
cancelPending() {
|
|
335
|
+
if (this.debounceTimer) {
|
|
336
|
+
clearTimeout(this.debounceTimer);
|
|
337
|
+
this.debounceTimer = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
310
340
|
dispose() {
|
|
311
341
|
if (this.debounceTimer) {
|
|
312
342
|
clearTimeout(this.debounceTimer);
|
|
@@ -320,6 +350,466 @@ var HistoryManager = class {
|
|
|
320
350
|
}
|
|
321
351
|
};
|
|
322
352
|
|
|
353
|
+
// src/snapping.ts
|
|
354
|
+
var STROKE = "#22c55e";
|
|
355
|
+
var SnapManager = class {
|
|
356
|
+
constructor(canvas, events) {
|
|
357
|
+
this.canvas = canvas;
|
|
358
|
+
this.events = events;
|
|
359
|
+
this.onMoving = (e) => this.handleMoving(e.target);
|
|
360
|
+
this.onAfterRender = () => this.drawGuides();
|
|
361
|
+
this.onModified = () => this.clearGuides();
|
|
362
|
+
this.canvas.on("object:moving", this.onMoving);
|
|
363
|
+
this.canvas.on("after:render", this.onAfterRender);
|
|
364
|
+
this.canvas.on("mouse:up", this.onModified);
|
|
365
|
+
this.canvas.on("selection:cleared", this.onModified);
|
|
366
|
+
}
|
|
367
|
+
canvas;
|
|
368
|
+
events;
|
|
369
|
+
enabled = false;
|
|
370
|
+
threshold = 8;
|
|
371
|
+
guides = [];
|
|
372
|
+
onMoving;
|
|
373
|
+
onAfterRender;
|
|
374
|
+
onModified;
|
|
375
|
+
setEnabled(enabled) {
|
|
376
|
+
if (this.enabled === enabled) return;
|
|
377
|
+
this.enabled = enabled;
|
|
378
|
+
if (!enabled) this.clearGuides();
|
|
379
|
+
this.events.emit("snap:changed", { enabled });
|
|
380
|
+
}
|
|
381
|
+
isEnabled() {
|
|
382
|
+
return this.enabled;
|
|
383
|
+
}
|
|
384
|
+
setThreshold(px) {
|
|
385
|
+
this.threshold = Math.max(0, px);
|
|
386
|
+
}
|
|
387
|
+
dispose() {
|
|
388
|
+
this.canvas.off("object:moving", this.onMoving);
|
|
389
|
+
this.canvas.off("after:render", this.onAfterRender);
|
|
390
|
+
this.canvas.off("mouse:up", this.onModified);
|
|
391
|
+
this.canvas.off("selection:cleared", this.onModified);
|
|
392
|
+
this.guides = [];
|
|
393
|
+
}
|
|
394
|
+
handleMoving(target) {
|
|
395
|
+
if (!this.enabled || !target) return;
|
|
396
|
+
target.setCoords();
|
|
397
|
+
const r = target.getBoundingRect();
|
|
398
|
+
const cw = this.canvas.getWidth();
|
|
399
|
+
const ch = this.canvas.getHeight();
|
|
400
|
+
const { xs, ys } = collectCandidates(this.canvas, target, cw, ch);
|
|
401
|
+
this.guides = [];
|
|
402
|
+
const dx = bestSnap([r.left, r.left + r.width / 2, r.left + r.width], xs, this.threshold);
|
|
403
|
+
if (dx !== null) {
|
|
404
|
+
target.set("left", (target.left ?? 0) + dx.delta);
|
|
405
|
+
this.guides.push({ x: dx.line });
|
|
406
|
+
}
|
|
407
|
+
const dy = bestSnap([r.top, r.top + r.height / 2, r.top + r.height], ys, this.threshold);
|
|
408
|
+
if (dy !== null) {
|
|
409
|
+
target.set("top", (target.top ?? 0) + dy.delta);
|
|
410
|
+
this.guides.push({ y: dy.line });
|
|
411
|
+
}
|
|
412
|
+
target.setCoords();
|
|
413
|
+
}
|
|
414
|
+
drawGuides() {
|
|
415
|
+
if (!this.enabled || this.guides.length === 0) return;
|
|
416
|
+
const ctx = this.canvas.contextTop;
|
|
417
|
+
if (!ctx) return;
|
|
418
|
+
const retina = this.canvas.getRetinaScaling();
|
|
419
|
+
const cw = this.canvas.getWidth();
|
|
420
|
+
const ch = this.canvas.getHeight();
|
|
421
|
+
ctx.save();
|
|
422
|
+
ctx.setTransform(retina, 0, 0, retina, 0, 0);
|
|
423
|
+
ctx.lineWidth = 1;
|
|
424
|
+
ctx.strokeStyle = STROKE;
|
|
425
|
+
ctx.setLineDash([4, 4]);
|
|
426
|
+
ctx.beginPath();
|
|
427
|
+
for (const g of this.guides) {
|
|
428
|
+
if (g.x !== void 0) {
|
|
429
|
+
ctx.moveTo(g.x, 0);
|
|
430
|
+
ctx.lineTo(g.x, ch);
|
|
431
|
+
}
|
|
432
|
+
if (g.y !== void 0) {
|
|
433
|
+
ctx.moveTo(0, g.y);
|
|
434
|
+
ctx.lineTo(cw, g.y);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
ctx.stroke();
|
|
438
|
+
ctx.restore();
|
|
439
|
+
}
|
|
440
|
+
clearGuides() {
|
|
441
|
+
if (this.guides.length === 0) return;
|
|
442
|
+
this.guides = [];
|
|
443
|
+
this.canvas.clearContext(this.canvas.contextTop);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function collectCandidates(canvas, moving, cw, ch) {
|
|
447
|
+
const xs = [0, cw / 2, cw];
|
|
448
|
+
const ys = [0, ch / 2, ch];
|
|
449
|
+
for (const obj of canvas.getObjects()) {
|
|
450
|
+
if (obj === moving || !obj.visible) continue;
|
|
451
|
+
obj.setCoords();
|
|
452
|
+
const r = obj.getBoundingRect();
|
|
453
|
+
xs.push(r.left, r.left + r.width / 2, r.left + r.width);
|
|
454
|
+
ys.push(r.top, r.top + r.height / 2, r.top + r.height);
|
|
455
|
+
}
|
|
456
|
+
return { xs, ys };
|
|
457
|
+
}
|
|
458
|
+
function bestSnap(edges, candidates, threshold) {
|
|
459
|
+
let best = null;
|
|
460
|
+
for (const edge of edges) {
|
|
461
|
+
for (const c of candidates) {
|
|
462
|
+
const dist = Math.abs(edge - c);
|
|
463
|
+
if (dist <= threshold && (best === null || dist < best.dist)) {
|
|
464
|
+
best = { delta: c - edge, line: c, dist };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return best ? { delta: best.delta, line: best.line } : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/crop.ts
|
|
472
|
+
var import_fabric = require("fabric");
|
|
473
|
+
|
|
474
|
+
// src/utils/clamp.ts
|
|
475
|
+
function clamp(v, min, max) {
|
|
476
|
+
return Math.min(max, Math.max(min, v));
|
|
477
|
+
}
|
|
478
|
+
function round2(v) {
|
|
479
|
+
return Math.round(v * 100) / 100;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/crop.ts
|
|
483
|
+
var CropController = class {
|
|
484
|
+
constructor(canvas, history, events) {
|
|
485
|
+
this.canvas = canvas;
|
|
486
|
+
this.history = history;
|
|
487
|
+
this.events = events;
|
|
488
|
+
this.onSelectionCleared = () => {
|
|
489
|
+
if (this.session) this.cancel();
|
|
490
|
+
};
|
|
491
|
+
this.canvas.on("selection:cleared", this.onSelectionCleared);
|
|
492
|
+
}
|
|
493
|
+
canvas;
|
|
494
|
+
history;
|
|
495
|
+
events;
|
|
496
|
+
session = null;
|
|
497
|
+
onSelectionCleared;
|
|
498
|
+
isActive() {
|
|
499
|
+
return this.session !== null;
|
|
500
|
+
}
|
|
501
|
+
activeLayerId() {
|
|
502
|
+
return this.session?.layerId ?? null;
|
|
503
|
+
}
|
|
504
|
+
start(layer) {
|
|
505
|
+
if (layer.type !== "image") return;
|
|
506
|
+
if (this.session) this.cancel();
|
|
507
|
+
const image = layer.fabricObject;
|
|
508
|
+
const prevAngle = image.angle ?? 0;
|
|
509
|
+
if (prevAngle) image.rotate(0);
|
|
510
|
+
image.setCoords();
|
|
511
|
+
const b = image.getBoundingRect();
|
|
512
|
+
const rect = new import_fabric.Rect({
|
|
513
|
+
left: b.left,
|
|
514
|
+
top: b.top,
|
|
515
|
+
width: b.width,
|
|
516
|
+
height: b.height,
|
|
517
|
+
originX: "left",
|
|
518
|
+
originY: "top",
|
|
519
|
+
fill: "rgba(34,197,94,0.12)",
|
|
520
|
+
stroke: STROKE2,
|
|
521
|
+
strokeWidth: 1,
|
|
522
|
+
strokeDashArray: [4, 4],
|
|
523
|
+
strokeUniform: true,
|
|
524
|
+
lockRotation: true,
|
|
525
|
+
cornerColor: STROKE2,
|
|
526
|
+
transparentCorners: false
|
|
527
|
+
});
|
|
528
|
+
rect.setControlsVisibility({ mtr: false });
|
|
529
|
+
const prevSelectable = image.selectable ?? true;
|
|
530
|
+
const prevEvented = image.evented ?? true;
|
|
531
|
+
image.selectable = false;
|
|
532
|
+
image.evented = false;
|
|
533
|
+
this.canvas.add(rect);
|
|
534
|
+
this.canvas.setActiveObject(rect);
|
|
535
|
+
this.canvas.requestRenderAll();
|
|
536
|
+
this.session = { layerId: layer.id, image, rect, prevSelectable, prevEvented, prevAngle };
|
|
537
|
+
this.events.emit("crop:changed", { active: true, layerId: layer.id });
|
|
538
|
+
}
|
|
539
|
+
apply() {
|
|
540
|
+
const s = this.session;
|
|
541
|
+
if (!s) return;
|
|
542
|
+
const { image, rect } = s;
|
|
543
|
+
rect.setCoords();
|
|
544
|
+
image.setCoords();
|
|
545
|
+
const scaleX = image.scaleX ?? 1;
|
|
546
|
+
const scaleY = image.scaleY ?? 1;
|
|
547
|
+
const imgLeft = image.left ?? 0;
|
|
548
|
+
const imgTop = image.top ?? 0;
|
|
549
|
+
const el = image.getElement();
|
|
550
|
+
const naturalW = el.naturalWidth || image.width || 0;
|
|
551
|
+
const naturalH = el.naturalHeight || image.height || 0;
|
|
552
|
+
let cropX = (image.cropX ?? 0) + (rect.left - imgLeft) / scaleX;
|
|
553
|
+
let cropY = (image.cropY ?? 0) + (rect.top - imgTop) / scaleY;
|
|
554
|
+
let cropW = rect.getScaledWidth() / scaleX;
|
|
555
|
+
let cropH = rect.getScaledHeight() / scaleY;
|
|
556
|
+
cropX = clamp(cropX, 0, Math.max(0, naturalW - 1));
|
|
557
|
+
cropY = clamp(cropY, 0, Math.max(0, naturalH - 1));
|
|
558
|
+
cropW = clamp(cropW, 1, naturalW - cropX);
|
|
559
|
+
cropH = clamp(cropH, 1, naturalH - cropY);
|
|
560
|
+
image.set({
|
|
561
|
+
cropX,
|
|
562
|
+
cropY,
|
|
563
|
+
width: cropW,
|
|
564
|
+
height: cropH,
|
|
565
|
+
// Anchor the displayed result where the rect sits.
|
|
566
|
+
left: imgLeft + (cropX - (image.cropX ?? 0)) * scaleX,
|
|
567
|
+
top: imgTop + (cropY - (image.cropY ?? 0)) * scaleY
|
|
568
|
+
});
|
|
569
|
+
image.setCoords();
|
|
570
|
+
this.finish();
|
|
571
|
+
this.history.save();
|
|
572
|
+
}
|
|
573
|
+
cancel() {
|
|
574
|
+
const s = this.session;
|
|
575
|
+
if (!s) return;
|
|
576
|
+
if (s.prevAngle) {
|
|
577
|
+
s.image.rotate(s.prevAngle);
|
|
578
|
+
s.image.setCoords();
|
|
579
|
+
}
|
|
580
|
+
this.finish();
|
|
581
|
+
}
|
|
582
|
+
dispose() {
|
|
583
|
+
this.cancel();
|
|
584
|
+
this.canvas.off("selection:cleared", this.onSelectionCleared);
|
|
585
|
+
}
|
|
586
|
+
finish() {
|
|
587
|
+
const s = this.session;
|
|
588
|
+
if (!s) return;
|
|
589
|
+
this.session = null;
|
|
590
|
+
this.canvas.remove(s.rect);
|
|
591
|
+
s.image.selectable = s.prevSelectable;
|
|
592
|
+
s.image.evented = s.prevEvented;
|
|
593
|
+
this.canvas.setActiveObject(s.image);
|
|
594
|
+
this.canvas.requestRenderAll();
|
|
595
|
+
this.events.emit("crop:changed", { active: false, layerId: null });
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
var STROKE2 = "#22c55e";
|
|
599
|
+
|
|
600
|
+
// src/pattern.ts
|
|
601
|
+
var PatternManager = class {
|
|
602
|
+
constructor(canvas, layers, history) {
|
|
603
|
+
this.canvas = canvas;
|
|
604
|
+
this.layers = layers;
|
|
605
|
+
this.history = history;
|
|
606
|
+
}
|
|
607
|
+
canvas;
|
|
608
|
+
layers;
|
|
609
|
+
history;
|
|
610
|
+
// Per-layer task chain. apply()/disable() both await an async setSrc on the
|
|
611
|
+
// same fabric image; running two concurrently lets their setSrc resolutions
|
|
612
|
+
// interleave (wrong image installed, original lost). Serialising per layer
|
|
613
|
+
// guarantees the last-requested operation wins and state stays consistent.
|
|
614
|
+
chains = /* @__PURE__ */ new Map();
|
|
615
|
+
isPattern(layerId) {
|
|
616
|
+
return !!this.layers.get(layerId)?.meta.pattern;
|
|
617
|
+
}
|
|
618
|
+
getConfig(layerId) {
|
|
619
|
+
return this.layers.get(layerId)?.meta.pattern?.config ?? null;
|
|
620
|
+
}
|
|
621
|
+
/** Turn a plain image layer into a pattern, or update an existing one. */
|
|
622
|
+
apply(layerId, config) {
|
|
623
|
+
return this.enqueue(layerId, async () => {
|
|
624
|
+
const layer = this.layers.get(layerId);
|
|
625
|
+
if (!layer || layer.type !== "image") return;
|
|
626
|
+
const image = layer.fabricObject;
|
|
627
|
+
const firstEnable = !layer.meta.pattern;
|
|
628
|
+
if (!layer.meta.pattern) {
|
|
629
|
+
layer.meta.pattern = {
|
|
630
|
+
config,
|
|
631
|
+
originalSrc: elementToDataURL(image) ?? image.getSrc(),
|
|
632
|
+
original: {
|
|
633
|
+
left: image.left ?? 0,
|
|
634
|
+
top: image.top ?? 0,
|
|
635
|
+
scaleX: image.scaleX ?? 1,
|
|
636
|
+
scaleY: image.scaleY ?? 1,
|
|
637
|
+
width: image.width ?? 0,
|
|
638
|
+
height: image.height ?? 0,
|
|
639
|
+
angle: image.angle ?? 0,
|
|
640
|
+
cropX: image.cropX ?? 0,
|
|
641
|
+
cropY: image.cropY ?? 0
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
} else {
|
|
645
|
+
layer.meta.pattern.config = config;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
await this.renderLayer(layer);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
if (firstEnable) delete layer.meta.pattern;
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
this.history.save();
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/** Restore the original image and drop the pattern. */
|
|
657
|
+
disable(layerId) {
|
|
658
|
+
return this.enqueue(layerId, async () => {
|
|
659
|
+
const layer = this.layers.get(layerId);
|
|
660
|
+
const state = layer?.meta.pattern;
|
|
661
|
+
if (!layer || !state) return;
|
|
662
|
+
const image = layer.fabricObject;
|
|
663
|
+
await image.setSrc(state.originalSrc);
|
|
664
|
+
image.set({
|
|
665
|
+
left: state.original.left,
|
|
666
|
+
top: state.original.top,
|
|
667
|
+
scaleX: state.original.scaleX,
|
|
668
|
+
scaleY: state.original.scaleY,
|
|
669
|
+
width: state.original.width,
|
|
670
|
+
height: state.original.height,
|
|
671
|
+
cropX: state.original.cropX,
|
|
672
|
+
cropY: state.original.cropY,
|
|
673
|
+
angle: state.original.angle
|
|
674
|
+
});
|
|
675
|
+
image.setCoords();
|
|
676
|
+
delete layer.meta.pattern;
|
|
677
|
+
this.canvas.requestRenderAll();
|
|
678
|
+
this.history.save();
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
/** Run `task` after any in-flight work for this layer, regardless of outcome. */
|
|
682
|
+
enqueue(layerId, task) {
|
|
683
|
+
const prev = this.chains.get(layerId) ?? Promise.resolve();
|
|
684
|
+
const next = prev.then(task, task);
|
|
685
|
+
this.chains.set(
|
|
686
|
+
layerId,
|
|
687
|
+
next.catch(() => void 0)
|
|
688
|
+
);
|
|
689
|
+
return next;
|
|
690
|
+
}
|
|
691
|
+
async renderLayer(layer) {
|
|
692
|
+
const state = layer.meta.pattern;
|
|
693
|
+
if (!state) return;
|
|
694
|
+
const image = layer.fabricObject;
|
|
695
|
+
const cw = this.canvas.getWidth();
|
|
696
|
+
const ch = this.canvas.getHeight();
|
|
697
|
+
const tileW = state.original.width * state.original.scaleX;
|
|
698
|
+
const tileH = state.original.height * state.original.scaleY;
|
|
699
|
+
const dataUrl = await buildPatternDataURL(
|
|
700
|
+
state.originalSrc,
|
|
701
|
+
state.config,
|
|
702
|
+
cw,
|
|
703
|
+
ch,
|
|
704
|
+
tileW,
|
|
705
|
+
tileH
|
|
706
|
+
);
|
|
707
|
+
await image.setSrc(dataUrl);
|
|
708
|
+
image.set({ left: 0, top: 0, scaleX: 1, scaleY: 1, cropX: 0, cropY: 0, angle: 0 });
|
|
709
|
+
image.setCoords();
|
|
710
|
+
this.canvas.requestRenderAll();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function elementToDataURL(image) {
|
|
714
|
+
try {
|
|
715
|
+
const el = image.getElement();
|
|
716
|
+
const w = el.naturalWidth || el.width;
|
|
717
|
+
const h = el.naturalHeight || el.height;
|
|
718
|
+
if (!w || !h) return null;
|
|
719
|
+
const off = document.createElement("canvas");
|
|
720
|
+
off.width = w;
|
|
721
|
+
off.height = h;
|
|
722
|
+
const ctx = off.getContext("2d");
|
|
723
|
+
if (!ctx) return null;
|
|
724
|
+
ctx.drawImage(el, 0, 0);
|
|
725
|
+
return off.toDataURL("image/png");
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
var IMAGE_CACHE_MAX = 16;
|
|
731
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
732
|
+
function loadImage(src) {
|
|
733
|
+
const cached = imageCache.get(src);
|
|
734
|
+
if (cached) {
|
|
735
|
+
imageCache.delete(src);
|
|
736
|
+
imageCache.set(src, cached);
|
|
737
|
+
return cached;
|
|
738
|
+
}
|
|
739
|
+
const promise = decodeImage(src);
|
|
740
|
+
promise.catch(() => {
|
|
741
|
+
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
742
|
+
});
|
|
743
|
+
imageCache.set(src, promise);
|
|
744
|
+
if (imageCache.size > IMAGE_CACHE_MAX) {
|
|
745
|
+
const oldest = imageCache.keys().next().value;
|
|
746
|
+
if (oldest !== void 0) imageCache.delete(oldest);
|
|
747
|
+
}
|
|
748
|
+
return promise;
|
|
749
|
+
}
|
|
750
|
+
function decodeImage(src) {
|
|
751
|
+
return new Promise((resolve, reject) => {
|
|
752
|
+
const img = new Image();
|
|
753
|
+
img.crossOrigin = "anonymous";
|
|
754
|
+
img.onload = () => resolve(img);
|
|
755
|
+
img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
|
|
756
|
+
img.src = src;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function clearPatternImageCache() {
|
|
760
|
+
imageCache.clear();
|
|
761
|
+
}
|
|
762
|
+
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH) {
|
|
763
|
+
const img = await loadImage(src);
|
|
764
|
+
const off = document.createElement("canvas");
|
|
765
|
+
off.width = Math.max(1, Math.round(targetW));
|
|
766
|
+
off.height = Math.max(1, Math.round(targetH));
|
|
767
|
+
const ctx = off.getContext("2d");
|
|
768
|
+
if (!ctx) return off.toDataURL("image/png");
|
|
769
|
+
drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
|
|
770
|
+
return off.toDataURL("image/png");
|
|
771
|
+
}
|
|
772
|
+
function computeTilePositions(config, targetW, targetH, baseW, baseH) {
|
|
773
|
+
const tileW = Math.max(1, baseW * (1 + config.horizontalSpacing / 100));
|
|
774
|
+
const tileH = Math.max(1, baseH * (1 + config.verticalSpacing / 100));
|
|
775
|
+
const diag = Math.sqrt(targetW * targetW + targetH * targetH);
|
|
776
|
+
const cols = Math.ceil(diag / tileW) + 2;
|
|
777
|
+
const rows = Math.ceil(diag / tileH) + 2;
|
|
778
|
+
const halfCols = Math.ceil(cols / 2);
|
|
779
|
+
const halfRows = Math.ceil(rows / 2);
|
|
780
|
+
const placements = [];
|
|
781
|
+
for (let j = -halfRows; j <= halfRows; j++) {
|
|
782
|
+
for (let i = -halfCols; i <= halfCols; i++) {
|
|
783
|
+
let x = i * tileW;
|
|
784
|
+
let y = j * tileH;
|
|
785
|
+
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
786
|
+
x += tileW * (config.horizontalOffset / 100);
|
|
787
|
+
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
788
|
+
y += tileH * (config.horizontalOffset / 100);
|
|
789
|
+
}
|
|
790
|
+
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
791
|
+
placements.push({ x, y, rotation });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return placements;
|
|
795
|
+
}
|
|
796
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
|
|
797
|
+
ctx.save();
|
|
798
|
+
ctx.translate(targetW / 2, targetH / 2);
|
|
799
|
+
ctx.rotate(config.angle * Math.PI / 180);
|
|
800
|
+
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
|
|
801
|
+
ctx.save();
|
|
802
|
+
ctx.translate(tile.x, tile.y);
|
|
803
|
+
ctx.rotate(tile.rotation);
|
|
804
|
+
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
805
|
+
ctx.restore();
|
|
806
|
+
}
|
|
807
|
+
ctx.restore();
|
|
808
|
+
}
|
|
809
|
+
function mod2(n) {
|
|
810
|
+
return (n % 2 + 2) % 2;
|
|
811
|
+
}
|
|
812
|
+
|
|
323
813
|
// src/utils/units.ts
|
|
324
814
|
var MM_PER_INCH = 25.4;
|
|
325
815
|
var UnitConverter = class {
|
|
@@ -366,7 +856,7 @@ var UnitConverter = class {
|
|
|
366
856
|
};
|
|
367
857
|
|
|
368
858
|
// src/serialization.ts
|
|
369
|
-
var
|
|
859
|
+
var import_fabric2 = require("fabric");
|
|
370
860
|
var VERSION = "1.0.0";
|
|
371
861
|
function serializeEditor(editor) {
|
|
372
862
|
return {
|
|
@@ -376,14 +866,20 @@ function serializeEditor(editor) {
|
|
|
376
866
|
height: editor.canvas.getHeight()
|
|
377
867
|
},
|
|
378
868
|
layers: editor.layers.getAll().map((layer) => layer.serialize()),
|
|
379
|
-
background
|
|
869
|
+
// The configured design background, not the live canvas value (which is
|
|
870
|
+
// forced transparent while a mockup preview is active).
|
|
871
|
+
background: editor.getDesignBackground(),
|
|
872
|
+
mockup: editor.getMockup()
|
|
380
873
|
};
|
|
381
874
|
}
|
|
382
875
|
async function deserializeEditor(editor, state) {
|
|
383
876
|
editor.layers.clear();
|
|
384
877
|
editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
|
|
385
878
|
if (state.background) {
|
|
386
|
-
editor.
|
|
879
|
+
editor.setBackground(state.background);
|
|
880
|
+
}
|
|
881
|
+
if (state.mockup !== void 0) {
|
|
882
|
+
editor.setMockup(state.mockup);
|
|
387
883
|
}
|
|
388
884
|
for (const serializedLayer of state.layers) {
|
|
389
885
|
await restoreLayer(editor, serializedLayer);
|
|
@@ -391,14 +887,12 @@ async function deserializeEditor(editor, state) {
|
|
|
391
887
|
editor.canvas.requestRenderAll();
|
|
392
888
|
}
|
|
393
889
|
async function restoreLayer(editor, serialized) {
|
|
394
|
-
const objects = await
|
|
890
|
+
const objects = await import_fabric2.util.enlivenObjects([serialized.fabricObject]);
|
|
395
891
|
const fabricObject = objects[0];
|
|
396
|
-
const layer = editor.layers.add(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
serialized.id
|
|
401
|
-
);
|
|
892
|
+
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
893
|
+
if (serialized.meta) {
|
|
894
|
+
layer.meta = serialized.meta;
|
|
895
|
+
}
|
|
402
896
|
if (!serialized.visible) {
|
|
403
897
|
editor.layers.setVisibility(layer.id, false);
|
|
404
898
|
}
|
|
@@ -429,32 +923,36 @@ function exportDataURL(canvas, format = "png", multiplier = 1) {
|
|
|
429
923
|
return canvas.toDataURL({ format, multiplier });
|
|
430
924
|
}
|
|
431
925
|
|
|
432
|
-
// src/utils/clamp.ts
|
|
433
|
-
function clamp(v, min, max) {
|
|
434
|
-
return Math.min(max, Math.max(min, v));
|
|
435
|
-
}
|
|
436
|
-
function round2(v) {
|
|
437
|
-
return Math.round(v * 100) / 100;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
926
|
// src/editor.ts
|
|
927
|
+
var MIN_ZOOM = 0.1;
|
|
928
|
+
var MAX_ZOOM = 8;
|
|
441
929
|
var CanvasEditor = class {
|
|
442
930
|
canvas;
|
|
443
931
|
layers;
|
|
444
932
|
history;
|
|
445
933
|
events;
|
|
446
934
|
units;
|
|
935
|
+
snapping;
|
|
936
|
+
crop;
|
|
937
|
+
patterns;
|
|
447
938
|
fileAdapter;
|
|
448
939
|
imageProvider;
|
|
940
|
+
zoomLevel = 1;
|
|
941
|
+
mockup = null;
|
|
942
|
+
// The design's configured background. The live canvas background is forced
|
|
943
|
+
// transparent while a mockup preview is shown, so this is the source of truth
|
|
944
|
+
// for serialization and export — not the (possibly transient) canvas value.
|
|
945
|
+
designBackground;
|
|
449
946
|
constructor(canvasElement, config) {
|
|
450
947
|
this.events = new EventEmitter();
|
|
451
948
|
this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
|
|
452
949
|
const widthPx = this.units.toPixels(config.width);
|
|
453
950
|
const heightPx = this.units.toPixels(config.height);
|
|
454
|
-
this.
|
|
951
|
+
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
952
|
+
this.canvas = new import_fabric3.Canvas(canvasElement, {
|
|
455
953
|
width: widthPx,
|
|
456
954
|
height: heightPx,
|
|
457
|
-
backgroundColor:
|
|
955
|
+
backgroundColor: this.designBackground,
|
|
458
956
|
preserveObjectStacking: config.preserveObjectStacking ?? true,
|
|
459
957
|
selection: true
|
|
460
958
|
});
|
|
@@ -469,18 +967,21 @@ var CanvasEditor = class {
|
|
|
469
967
|
},
|
|
470
968
|
events: this.events
|
|
471
969
|
});
|
|
970
|
+
this.snapping = new SnapManager(this.canvas, this.events);
|
|
971
|
+
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
972
|
+
this.patterns = new PatternManager(this.canvas, this.layers, this.history);
|
|
472
973
|
this.setupCanvasEvents();
|
|
473
974
|
this.history.saveImmediate();
|
|
474
975
|
}
|
|
475
976
|
// ─── Layer Operations ────────────────────────────────
|
|
476
977
|
async addImage(url, options) {
|
|
477
|
-
const img = await
|
|
978
|
+
const img = await import_fabric3.FabricImage.fromURL(url, {}, options);
|
|
478
979
|
const layer = this.layers.add("image", img);
|
|
479
980
|
this.history.save();
|
|
480
981
|
return layer;
|
|
481
982
|
}
|
|
482
983
|
addText(text, options) {
|
|
483
|
-
const textbox = new
|
|
984
|
+
const textbox = new import_fabric3.Textbox(text, {
|
|
484
985
|
fontSize: 32,
|
|
485
986
|
fontFamily: "Arial",
|
|
486
987
|
fill: "#000000",
|
|
@@ -505,7 +1006,11 @@ var CanvasEditor = class {
|
|
|
505
1006
|
fill: "#666666",
|
|
506
1007
|
width: 300
|
|
507
1008
|
});
|
|
508
|
-
const layer = this.layers.add(
|
|
1009
|
+
const layer = this.layers.add(
|
|
1010
|
+
"template",
|
|
1011
|
+
placeholder,
|
|
1012
|
+
template.name
|
|
1013
|
+
);
|
|
509
1014
|
this.history.save();
|
|
510
1015
|
return layer;
|
|
511
1016
|
}
|
|
@@ -521,6 +1026,36 @@ var CanvasEditor = class {
|
|
|
521
1026
|
if (!active) return null;
|
|
522
1027
|
return this.layers.findByObject(active) ?? null;
|
|
523
1028
|
}
|
|
1029
|
+
/** Mirror a layer along the given axis. */
|
|
1030
|
+
flipLayer(id, axis) {
|
|
1031
|
+
const layer = this.layers.get(id);
|
|
1032
|
+
if (!layer) return;
|
|
1033
|
+
const obj = layer.fabricObject;
|
|
1034
|
+
if (axis === "horizontal") {
|
|
1035
|
+
obj.set("flipX", !obj.flipX);
|
|
1036
|
+
} else {
|
|
1037
|
+
obj.set("flipY", !obj.flipY);
|
|
1038
|
+
}
|
|
1039
|
+
obj.setCoords();
|
|
1040
|
+
this.canvas.requestRenderAll();
|
|
1041
|
+
this.history.save();
|
|
1042
|
+
}
|
|
1043
|
+
/** Clone a layer (offset slightly) and select the copy. */
|
|
1044
|
+
async duplicateLayer(id) {
|
|
1045
|
+
const layer = this.layers.get(id);
|
|
1046
|
+
if (!layer) return null;
|
|
1047
|
+
const clone = await layer.fabricObject.clone();
|
|
1048
|
+
if (!layer.meta.pattern) {
|
|
1049
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
1050
|
+
}
|
|
1051
|
+
clone.setCoords();
|
|
1052
|
+
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
1053
|
+
copy.meta = structuredClone(layer.meta);
|
|
1054
|
+
this.canvas.setActiveObject(clone);
|
|
1055
|
+
this.canvas.requestRenderAll();
|
|
1056
|
+
this.history.save();
|
|
1057
|
+
return copy;
|
|
1058
|
+
}
|
|
524
1059
|
// ─── Serialization ──────────────────────────────────
|
|
525
1060
|
toJSON() {
|
|
526
1061
|
return serializeEditor(this);
|
|
@@ -531,18 +1066,33 @@ var CanvasEditor = class {
|
|
|
531
1066
|
// ─── Export ──────────────────────────────────────────
|
|
532
1067
|
async toPNG(options) {
|
|
533
1068
|
this.events.emit("export:start", { format: "png" });
|
|
534
|
-
const blob = await exportPNG(this.canvas, options);
|
|
1069
|
+
const blob = await this.withDesignBackground(() => exportPNG(this.canvas, options));
|
|
535
1070
|
this.events.emit("export:complete", { format: "png" });
|
|
536
1071
|
return blob;
|
|
537
1072
|
}
|
|
538
1073
|
toSVG() {
|
|
539
1074
|
this.events.emit("export:start", { format: "svg" });
|
|
540
|
-
const svg = exportSVG(this.canvas);
|
|
1075
|
+
const svg = this.withDesignBackground(() => exportSVG(this.canvas));
|
|
541
1076
|
this.events.emit("export:complete", { format: "svg" });
|
|
542
1077
|
return svg;
|
|
543
1078
|
}
|
|
544
1079
|
toDataURL(format = "png", multiplier = 1) {
|
|
545
|
-
return exportDataURL(this.canvas, format, multiplier);
|
|
1080
|
+
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Run an export with the configured design background applied, even when a
|
|
1084
|
+
* mockup preview has forced the live canvas transparent — so exports reflect
|
|
1085
|
+
* the design, not the preview. Restores the preview state afterwards.
|
|
1086
|
+
*/
|
|
1087
|
+
withDesignBackground(fn) {
|
|
1088
|
+
if (!this.mockup) return fn();
|
|
1089
|
+
const previewBg = this.canvas.backgroundColor;
|
|
1090
|
+
this.canvas.backgroundColor = this.designBackground;
|
|
1091
|
+
try {
|
|
1092
|
+
return fn();
|
|
1093
|
+
} finally {
|
|
1094
|
+
this.canvas.backgroundColor = previewBg;
|
|
1095
|
+
}
|
|
546
1096
|
}
|
|
547
1097
|
toPrintifyPositioning() {
|
|
548
1098
|
const result = {};
|
|
@@ -594,22 +1144,82 @@ var CanvasEditor = class {
|
|
|
594
1144
|
}
|
|
595
1145
|
// ─── Canvas Operations ──────────────────────────────
|
|
596
1146
|
setBackground(color) {
|
|
597
|
-
this.
|
|
598
|
-
this.
|
|
1147
|
+
this.designBackground = color;
|
|
1148
|
+
if (!this.mockup) {
|
|
1149
|
+
this.canvas.backgroundColor = color;
|
|
1150
|
+
this.canvas.requestRenderAll();
|
|
1151
|
+
}
|
|
599
1152
|
this.history.save();
|
|
600
1153
|
}
|
|
1154
|
+
getDesignBackground() {
|
|
1155
|
+
return this.designBackground;
|
|
1156
|
+
}
|
|
601
1157
|
resize(width, height) {
|
|
602
1158
|
const widthPx = this.units.toPixels(width);
|
|
603
1159
|
const heightPx = this.units.toPixels(height);
|
|
604
1160
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
605
1161
|
this.canvas.requestRenderAll();
|
|
606
1162
|
}
|
|
1163
|
+
// ─── Zoom ───────────────────────────────────────────
|
|
1164
|
+
//
|
|
1165
|
+
// Zoom is a *display* transform (the view layer CSS-scales the canvas), not a
|
|
1166
|
+
// fabric viewport zoom — fabric's setZoom bakes into toDataURL/toSVG and would
|
|
1167
|
+
// corrupt exports. The editor only tracks the level and emits an event.
|
|
1168
|
+
setZoom(level) {
|
|
1169
|
+
const next = clamp(round2(level), MIN_ZOOM, MAX_ZOOM);
|
|
1170
|
+
if (next === this.zoomLevel) return;
|
|
1171
|
+
this.zoomLevel = next;
|
|
1172
|
+
this.events.emit("zoom:changed", { zoom: next });
|
|
1173
|
+
}
|
|
1174
|
+
/** @deprecated use setZoom — kept for backward compatibility. */
|
|
607
1175
|
zoom(level) {
|
|
608
|
-
this.
|
|
1176
|
+
this.setZoom(level);
|
|
1177
|
+
}
|
|
1178
|
+
getZoom() {
|
|
1179
|
+
return this.zoomLevel;
|
|
1180
|
+
}
|
|
1181
|
+
zoomIn(step = 0.1) {
|
|
1182
|
+
this.setZoom(this.zoomLevel + step);
|
|
1183
|
+
}
|
|
1184
|
+
zoomOut(step = 0.1) {
|
|
1185
|
+
this.setZoom(this.zoomLevel - step);
|
|
1186
|
+
}
|
|
1187
|
+
resetZoom() {
|
|
1188
|
+
this.setZoom(1);
|
|
1189
|
+
}
|
|
1190
|
+
/** Fit the canvas inside a viewport (in px), accounting for padding. */
|
|
1191
|
+
zoomToFit(viewportWidth, viewportHeight, padding = 0) {
|
|
1192
|
+
const w = this.canvas.getWidth();
|
|
1193
|
+
const h = this.canvas.getHeight();
|
|
1194
|
+
if (w <= 0 || h <= 0) return;
|
|
1195
|
+
const sx = (viewportWidth - padding * 2) / w;
|
|
1196
|
+
const sy = (viewportHeight - padding * 2) / h;
|
|
1197
|
+
this.setZoom(Math.min(sx, sy));
|
|
1198
|
+
}
|
|
1199
|
+
// ─── Patterns ───────────────────────────────────────
|
|
1200
|
+
applyPattern(layerId, config) {
|
|
1201
|
+
return this.patterns.apply(layerId, config);
|
|
1202
|
+
}
|
|
1203
|
+
clearPattern(layerId) {
|
|
1204
|
+
return this.patterns.disable(layerId);
|
|
1205
|
+
}
|
|
1206
|
+
// ─── Mockup (preview-only) ──────────────────────────
|
|
1207
|
+
setMockup(mockup) {
|
|
1208
|
+
this.mockup = mockup;
|
|
1209
|
+
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
609
1210
|
this.canvas.requestRenderAll();
|
|
1211
|
+
this.events.emit("mockup:changed", { mockup });
|
|
1212
|
+
}
|
|
1213
|
+
clearMockup() {
|
|
1214
|
+
this.setMockup(null);
|
|
1215
|
+
}
|
|
1216
|
+
getMockup() {
|
|
1217
|
+
return this.mockup;
|
|
610
1218
|
}
|
|
611
1219
|
// ─── Cleanup ────────────────────────────────────────
|
|
612
1220
|
dispose() {
|
|
1221
|
+
this.snapping.dispose();
|
|
1222
|
+
this.crop.dispose();
|
|
613
1223
|
this.history.dispose();
|
|
614
1224
|
this.events.removeAllListeners();
|
|
615
1225
|
this.canvas.dispose();
|
|
@@ -637,16 +1247,35 @@ var CanvasEditor = class {
|
|
|
637
1247
|
});
|
|
638
1248
|
}
|
|
639
1249
|
};
|
|
1250
|
+
|
|
1251
|
+
// src/types.ts
|
|
1252
|
+
var DEFAULT_PATTERN_CONFIG = {
|
|
1253
|
+
mode: "grid",
|
|
1254
|
+
horizontalSpacing: 0,
|
|
1255
|
+
verticalSpacing: 0,
|
|
1256
|
+
angle: 0,
|
|
1257
|
+
horizontalOffset: 0,
|
|
1258
|
+
rotationStepH: 0,
|
|
1259
|
+
rotationStepV: 0
|
|
1260
|
+
};
|
|
640
1261
|
// Annotate the CommonJS export names for ESM import in node:
|
|
641
1262
|
0 && (module.exports = {
|
|
642
1263
|
CanvasEditor,
|
|
1264
|
+
CropController,
|
|
1265
|
+
DEFAULT_PATTERN_CONFIG,
|
|
643
1266
|
EventEmitter,
|
|
644
1267
|
HistoryManager,
|
|
645
1268
|
Layer,
|
|
646
1269
|
LayerManager,
|
|
1270
|
+
PatternManager,
|
|
1271
|
+
SnapManager,
|
|
647
1272
|
UnitConverter,
|
|
1273
|
+
buildPatternDataURL,
|
|
648
1274
|
clamp,
|
|
1275
|
+
clearPatternImageCache,
|
|
1276
|
+
computeTilePositions,
|
|
649
1277
|
deserializeEditor,
|
|
1278
|
+
drawTiles,
|
|
650
1279
|
exportDataURL,
|
|
651
1280
|
exportPNG,
|
|
652
1281
|
exportSVG,
|