@overtone-art/canvas-editor-core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +214 -1
- package/dist/index.d.ts +214 -1
- package/dist/index.js +664 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +651 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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() {
|
|
@@ -275,6 +290,7 @@ var HistoryManager = class {
|
|
|
275
290
|
this.emitChanged();
|
|
276
291
|
}
|
|
277
292
|
async undo() {
|
|
293
|
+
this.cancelPending();
|
|
278
294
|
const state = this.undoStack.pop();
|
|
279
295
|
if (!state) return;
|
|
280
296
|
this.redoStack.push(this.getState());
|
|
@@ -284,6 +300,7 @@ var HistoryManager = class {
|
|
|
284
300
|
this.emitChanged();
|
|
285
301
|
}
|
|
286
302
|
async redo() {
|
|
303
|
+
this.cancelPending();
|
|
287
304
|
const state = this.redoStack.pop();
|
|
288
305
|
if (!state) return;
|
|
289
306
|
this.undoStack.push(this.getState());
|
|
@@ -292,6 +309,11 @@ var HistoryManager = class {
|
|
|
292
309
|
this.paused = false;
|
|
293
310
|
this.emitChanged();
|
|
294
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
|
+
}
|
|
295
317
|
pause() {
|
|
296
318
|
this.paused = true;
|
|
297
319
|
}
|
|
@@ -309,6 +331,12 @@ var HistoryManager = class {
|
|
|
309
331
|
this.redoStack = [];
|
|
310
332
|
this.emitChanged();
|
|
311
333
|
}
|
|
334
|
+
cancelPending() {
|
|
335
|
+
if (this.debounceTimer) {
|
|
336
|
+
clearTimeout(this.debounceTimer);
|
|
337
|
+
this.debounceTimer = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
312
340
|
dispose() {
|
|
313
341
|
if (this.debounceTimer) {
|
|
314
342
|
clearTimeout(this.debounceTimer);
|
|
@@ -322,6 +350,466 @@ var HistoryManager = class {
|
|
|
322
350
|
}
|
|
323
351
|
};
|
|
324
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
|
+
|
|
325
813
|
// src/utils/units.ts
|
|
326
814
|
var MM_PER_INCH = 25.4;
|
|
327
815
|
var UnitConverter = class {
|
|
@@ -368,7 +856,7 @@ var UnitConverter = class {
|
|
|
368
856
|
};
|
|
369
857
|
|
|
370
858
|
// src/serialization.ts
|
|
371
|
-
var
|
|
859
|
+
var import_fabric2 = require("fabric");
|
|
372
860
|
var VERSION = "1.0.0";
|
|
373
861
|
function serializeEditor(editor) {
|
|
374
862
|
return {
|
|
@@ -378,14 +866,20 @@ function serializeEditor(editor) {
|
|
|
378
866
|
height: editor.canvas.getHeight()
|
|
379
867
|
},
|
|
380
868
|
layers: editor.layers.getAll().map((layer) => layer.serialize()),
|
|
381
|
-
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()
|
|
382
873
|
};
|
|
383
874
|
}
|
|
384
875
|
async function deserializeEditor(editor, state) {
|
|
385
876
|
editor.layers.clear();
|
|
386
877
|
editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
|
|
387
878
|
if (state.background) {
|
|
388
|
-
editor.
|
|
879
|
+
editor.setBackground(state.background);
|
|
880
|
+
}
|
|
881
|
+
if (state.mockup !== void 0) {
|
|
882
|
+
editor.setMockup(state.mockup);
|
|
389
883
|
}
|
|
390
884
|
for (const serializedLayer of state.layers) {
|
|
391
885
|
await restoreLayer(editor, serializedLayer);
|
|
@@ -393,14 +887,12 @@ async function deserializeEditor(editor, state) {
|
|
|
393
887
|
editor.canvas.requestRenderAll();
|
|
394
888
|
}
|
|
395
889
|
async function restoreLayer(editor, serialized) {
|
|
396
|
-
const objects = await
|
|
890
|
+
const objects = await import_fabric2.util.enlivenObjects([serialized.fabricObject]);
|
|
397
891
|
const fabricObject = objects[0];
|
|
398
|
-
const layer = editor.layers.add(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
serialized.id
|
|
403
|
-
);
|
|
892
|
+
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
893
|
+
if (serialized.meta) {
|
|
894
|
+
layer.meta = serialized.meta;
|
|
895
|
+
}
|
|
404
896
|
if (!serialized.visible) {
|
|
405
897
|
editor.layers.setVisibility(layer.id, false);
|
|
406
898
|
}
|
|
@@ -431,32 +923,36 @@ function exportDataURL(canvas, format = "png", multiplier = 1) {
|
|
|
431
923
|
return canvas.toDataURL({ format, multiplier });
|
|
432
924
|
}
|
|
433
925
|
|
|
434
|
-
// src/utils/clamp.ts
|
|
435
|
-
function clamp(v, min, max) {
|
|
436
|
-
return Math.min(max, Math.max(min, v));
|
|
437
|
-
}
|
|
438
|
-
function round2(v) {
|
|
439
|
-
return Math.round(v * 100) / 100;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
926
|
// src/editor.ts
|
|
927
|
+
var MIN_ZOOM = 0.1;
|
|
928
|
+
var MAX_ZOOM = 8;
|
|
443
929
|
var CanvasEditor = class {
|
|
444
930
|
canvas;
|
|
445
931
|
layers;
|
|
446
932
|
history;
|
|
447
933
|
events;
|
|
448
934
|
units;
|
|
935
|
+
snapping;
|
|
936
|
+
crop;
|
|
937
|
+
patterns;
|
|
449
938
|
fileAdapter;
|
|
450
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;
|
|
451
946
|
constructor(canvasElement, config) {
|
|
452
947
|
this.events = new EventEmitter();
|
|
453
948
|
this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
|
|
454
949
|
const widthPx = this.units.toPixels(config.width);
|
|
455
950
|
const heightPx = this.units.toPixels(config.height);
|
|
456
|
-
this.
|
|
951
|
+
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
952
|
+
this.canvas = new import_fabric3.Canvas(canvasElement, {
|
|
457
953
|
width: widthPx,
|
|
458
954
|
height: heightPx,
|
|
459
|
-
backgroundColor:
|
|
955
|
+
backgroundColor: this.designBackground,
|
|
460
956
|
preserveObjectStacking: config.preserveObjectStacking ?? true,
|
|
461
957
|
selection: true
|
|
462
958
|
});
|
|
@@ -471,22 +967,28 @@ var CanvasEditor = class {
|
|
|
471
967
|
},
|
|
472
968
|
events: this.events
|
|
473
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);
|
|
474
973
|
this.setupCanvasEvents();
|
|
475
974
|
this.history.saveImmediate();
|
|
476
975
|
}
|
|
477
976
|
// ─── Layer Operations ────────────────────────────────
|
|
478
977
|
async addImage(url, options) {
|
|
479
|
-
const img = await
|
|
978
|
+
const img = await import_fabric3.FabricImage.fromURL(url, {}, { originX: "left", originY: "top", ...options });
|
|
480
979
|
const layer = this.layers.add("image", img);
|
|
481
980
|
this.history.save();
|
|
482
981
|
return layer;
|
|
483
982
|
}
|
|
484
983
|
addText(text, options) {
|
|
485
|
-
const textbox = new
|
|
984
|
+
const textbox = new import_fabric3.Textbox(text, {
|
|
486
985
|
fontSize: 32,
|
|
487
986
|
fontFamily: "Arial",
|
|
488
987
|
fill: "#000000",
|
|
489
988
|
width: 200,
|
|
989
|
+
// v7 defaults origin to center; keep top-left placement.
|
|
990
|
+
originX: "left",
|
|
991
|
+
originY: "top",
|
|
490
992
|
...options
|
|
491
993
|
});
|
|
492
994
|
const layer = this.layers.add("text", textbox);
|
|
@@ -505,9 +1007,15 @@ var CanvasEditor = class {
|
|
|
505
1007
|
fontSize: 24,
|
|
506
1008
|
fontFamily: "Arial",
|
|
507
1009
|
fill: "#666666",
|
|
508
|
-
width: 300
|
|
1010
|
+
width: 300,
|
|
1011
|
+
originX: "left",
|
|
1012
|
+
originY: "top"
|
|
509
1013
|
});
|
|
510
|
-
const layer = this.layers.add(
|
|
1014
|
+
const layer = this.layers.add(
|
|
1015
|
+
"template",
|
|
1016
|
+
placeholder,
|
|
1017
|
+
template.name
|
|
1018
|
+
);
|
|
511
1019
|
this.history.save();
|
|
512
1020
|
return layer;
|
|
513
1021
|
}
|
|
@@ -523,6 +1031,36 @@ var CanvasEditor = class {
|
|
|
523
1031
|
if (!active) return null;
|
|
524
1032
|
return this.layers.findByObject(active) ?? null;
|
|
525
1033
|
}
|
|
1034
|
+
/** Mirror a layer along the given axis. */
|
|
1035
|
+
flipLayer(id, axis) {
|
|
1036
|
+
const layer = this.layers.get(id);
|
|
1037
|
+
if (!layer) return;
|
|
1038
|
+
const obj = layer.fabricObject;
|
|
1039
|
+
if (axis === "horizontal") {
|
|
1040
|
+
obj.set("flipX", !obj.flipX);
|
|
1041
|
+
} else {
|
|
1042
|
+
obj.set("flipY", !obj.flipY);
|
|
1043
|
+
}
|
|
1044
|
+
obj.setCoords();
|
|
1045
|
+
this.canvas.requestRenderAll();
|
|
1046
|
+
this.history.save();
|
|
1047
|
+
}
|
|
1048
|
+
/** Clone a layer (offset slightly) and select the copy. */
|
|
1049
|
+
async duplicateLayer(id) {
|
|
1050
|
+
const layer = this.layers.get(id);
|
|
1051
|
+
if (!layer) return null;
|
|
1052
|
+
const clone = await layer.fabricObject.clone();
|
|
1053
|
+
if (!layer.meta.pattern) {
|
|
1054
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
1055
|
+
}
|
|
1056
|
+
clone.setCoords();
|
|
1057
|
+
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
1058
|
+
copy.meta = structuredClone(layer.meta);
|
|
1059
|
+
this.canvas.setActiveObject(clone);
|
|
1060
|
+
this.canvas.requestRenderAll();
|
|
1061
|
+
this.history.save();
|
|
1062
|
+
return copy;
|
|
1063
|
+
}
|
|
526
1064
|
// ─── Serialization ──────────────────────────────────
|
|
527
1065
|
toJSON() {
|
|
528
1066
|
return serializeEditor(this);
|
|
@@ -533,18 +1071,33 @@ var CanvasEditor = class {
|
|
|
533
1071
|
// ─── Export ──────────────────────────────────────────
|
|
534
1072
|
async toPNG(options) {
|
|
535
1073
|
this.events.emit("export:start", { format: "png" });
|
|
536
|
-
const blob = await exportPNG(this.canvas, options);
|
|
1074
|
+
const blob = await this.withDesignBackground(() => exportPNG(this.canvas, options));
|
|
537
1075
|
this.events.emit("export:complete", { format: "png" });
|
|
538
1076
|
return blob;
|
|
539
1077
|
}
|
|
540
1078
|
toSVG() {
|
|
541
1079
|
this.events.emit("export:start", { format: "svg" });
|
|
542
|
-
const svg = exportSVG(this.canvas);
|
|
1080
|
+
const svg = this.withDesignBackground(() => exportSVG(this.canvas));
|
|
543
1081
|
this.events.emit("export:complete", { format: "svg" });
|
|
544
1082
|
return svg;
|
|
545
1083
|
}
|
|
546
1084
|
toDataURL(format = "png", multiplier = 1) {
|
|
547
|
-
return exportDataURL(this.canvas, format, multiplier);
|
|
1085
|
+
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Run an export with the configured design background applied, even when a
|
|
1089
|
+
* mockup preview has forced the live canvas transparent — so exports reflect
|
|
1090
|
+
* the design, not the preview. Restores the preview state afterwards.
|
|
1091
|
+
*/
|
|
1092
|
+
withDesignBackground(fn) {
|
|
1093
|
+
if (!this.mockup) return fn();
|
|
1094
|
+
const previewBg = this.canvas.backgroundColor;
|
|
1095
|
+
this.canvas.backgroundColor = this.designBackground;
|
|
1096
|
+
try {
|
|
1097
|
+
return fn();
|
|
1098
|
+
} finally {
|
|
1099
|
+
this.canvas.backgroundColor = previewBg;
|
|
1100
|
+
}
|
|
548
1101
|
}
|
|
549
1102
|
toPrintifyPositioning() {
|
|
550
1103
|
const result = {};
|
|
@@ -596,22 +1149,82 @@ var CanvasEditor = class {
|
|
|
596
1149
|
}
|
|
597
1150
|
// ─── Canvas Operations ──────────────────────────────
|
|
598
1151
|
setBackground(color) {
|
|
599
|
-
this.
|
|
600
|
-
this.
|
|
1152
|
+
this.designBackground = color;
|
|
1153
|
+
if (!this.mockup) {
|
|
1154
|
+
this.canvas.backgroundColor = color;
|
|
1155
|
+
this.canvas.requestRenderAll();
|
|
1156
|
+
}
|
|
601
1157
|
this.history.save();
|
|
602
1158
|
}
|
|
1159
|
+
getDesignBackground() {
|
|
1160
|
+
return this.designBackground;
|
|
1161
|
+
}
|
|
603
1162
|
resize(width, height) {
|
|
604
1163
|
const widthPx = this.units.toPixels(width);
|
|
605
1164
|
const heightPx = this.units.toPixels(height);
|
|
606
1165
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
607
1166
|
this.canvas.requestRenderAll();
|
|
608
1167
|
}
|
|
1168
|
+
// ─── Zoom ───────────────────────────────────────────
|
|
1169
|
+
//
|
|
1170
|
+
// Zoom is a *display* transform (the view layer CSS-scales the canvas), not a
|
|
1171
|
+
// fabric viewport zoom — fabric's setZoom bakes into toDataURL/toSVG and would
|
|
1172
|
+
// corrupt exports. The editor only tracks the level and emits an event.
|
|
1173
|
+
setZoom(level) {
|
|
1174
|
+
const next = clamp(round2(level), MIN_ZOOM, MAX_ZOOM);
|
|
1175
|
+
if (next === this.zoomLevel) return;
|
|
1176
|
+
this.zoomLevel = next;
|
|
1177
|
+
this.events.emit("zoom:changed", { zoom: next });
|
|
1178
|
+
}
|
|
1179
|
+
/** @deprecated use setZoom — kept for backward compatibility. */
|
|
609
1180
|
zoom(level) {
|
|
610
|
-
this.
|
|
1181
|
+
this.setZoom(level);
|
|
1182
|
+
}
|
|
1183
|
+
getZoom() {
|
|
1184
|
+
return this.zoomLevel;
|
|
1185
|
+
}
|
|
1186
|
+
zoomIn(step = 0.1) {
|
|
1187
|
+
this.setZoom(this.zoomLevel + step);
|
|
1188
|
+
}
|
|
1189
|
+
zoomOut(step = 0.1) {
|
|
1190
|
+
this.setZoom(this.zoomLevel - step);
|
|
1191
|
+
}
|
|
1192
|
+
resetZoom() {
|
|
1193
|
+
this.setZoom(1);
|
|
1194
|
+
}
|
|
1195
|
+
/** Fit the canvas inside a viewport (in px), accounting for padding. */
|
|
1196
|
+
zoomToFit(viewportWidth, viewportHeight, padding = 0) {
|
|
1197
|
+
const w = this.canvas.getWidth();
|
|
1198
|
+
const h = this.canvas.getHeight();
|
|
1199
|
+
if (w <= 0 || h <= 0) return;
|
|
1200
|
+
const sx = (viewportWidth - padding * 2) / w;
|
|
1201
|
+
const sy = (viewportHeight - padding * 2) / h;
|
|
1202
|
+
this.setZoom(Math.min(sx, sy));
|
|
1203
|
+
}
|
|
1204
|
+
// ─── Patterns ───────────────────────────────────────
|
|
1205
|
+
applyPattern(layerId, config) {
|
|
1206
|
+
return this.patterns.apply(layerId, config);
|
|
1207
|
+
}
|
|
1208
|
+
clearPattern(layerId) {
|
|
1209
|
+
return this.patterns.disable(layerId);
|
|
1210
|
+
}
|
|
1211
|
+
// ─── Mockup (preview-only) ──────────────────────────
|
|
1212
|
+
setMockup(mockup) {
|
|
1213
|
+
this.mockup = mockup;
|
|
1214
|
+
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
611
1215
|
this.canvas.requestRenderAll();
|
|
1216
|
+
this.events.emit("mockup:changed", { mockup });
|
|
1217
|
+
}
|
|
1218
|
+
clearMockup() {
|
|
1219
|
+
this.setMockup(null);
|
|
1220
|
+
}
|
|
1221
|
+
getMockup() {
|
|
1222
|
+
return this.mockup;
|
|
612
1223
|
}
|
|
613
1224
|
// ─── Cleanup ────────────────────────────────────────
|
|
614
1225
|
dispose() {
|
|
1226
|
+
this.snapping.dispose();
|
|
1227
|
+
this.crop.dispose();
|
|
615
1228
|
this.history.dispose();
|
|
616
1229
|
this.events.removeAllListeners();
|
|
617
1230
|
this.canvas.dispose();
|
|
@@ -639,16 +1252,35 @@ var CanvasEditor = class {
|
|
|
639
1252
|
});
|
|
640
1253
|
}
|
|
641
1254
|
};
|
|
1255
|
+
|
|
1256
|
+
// src/types.ts
|
|
1257
|
+
var DEFAULT_PATTERN_CONFIG = {
|
|
1258
|
+
mode: "grid",
|
|
1259
|
+
horizontalSpacing: 0,
|
|
1260
|
+
verticalSpacing: 0,
|
|
1261
|
+
angle: 0,
|
|
1262
|
+
horizontalOffset: 0,
|
|
1263
|
+
rotationStepH: 0,
|
|
1264
|
+
rotationStepV: 0
|
|
1265
|
+
};
|
|
642
1266
|
// Annotate the CommonJS export names for ESM import in node:
|
|
643
1267
|
0 && (module.exports = {
|
|
644
1268
|
CanvasEditor,
|
|
1269
|
+
CropController,
|
|
1270
|
+
DEFAULT_PATTERN_CONFIG,
|
|
645
1271
|
EventEmitter,
|
|
646
1272
|
HistoryManager,
|
|
647
1273
|
Layer,
|
|
648
1274
|
LayerManager,
|
|
1275
|
+
PatternManager,
|
|
1276
|
+
SnapManager,
|
|
649
1277
|
UnitConverter,
|
|
1278
|
+
buildPatternDataURL,
|
|
650
1279
|
clamp,
|
|
1280
|
+
clearPatternImageCache,
|
|
1281
|
+
computeTilePositions,
|
|
651
1282
|
deserializeEditor,
|
|
1283
|
+
drawTiles,
|
|
652
1284
|
exportDataURL,
|
|
653
1285
|
exportPNG,
|
|
654
1286
|
exportSVG,
|