Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 70 additions & 21 deletions plugins/Image-Toolbox/core/src/CanvasManager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import eventBus from './EventBus.js';
import eventBus from './EventBus.js';

const CLIP_PATH_SERIALIZED_PROPS = ['clipPath', 'absolutePositioned', 'inverted'];

Expand All @@ -23,6 +23,8 @@ class CanvasManager {
this.zoomLevel = 1;
this._historySaveTimer = null;
this._isCropMode = false;
this._resizeObserver = null;
this._boundResize = null;
}

// ── 生命周期 ──
Expand All @@ -48,6 +50,14 @@ class CanvasManager {
* 销毁画布,释放内存
*/
destroy() {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
if (this._boundResize) {
window.removeEventListener('resize', this._boundResize);
this._boundResize = null;
}
if (this._historySaveTimer) {
clearTimeout(this._historySaveTimer);
}
Expand Down Expand Up @@ -96,7 +106,7 @@ class CanvasManager {
fabricImg.width, fabricImg.height, fabricImg.type);

this.originalImage = fabricImg;
fabricImg._originalImage = true;
this._applyBackgroundImageProps(fabricImg);
this.canvas.clear();
this.canvas.add(fabricImg);
this.canvas.renderAll();
Expand Down Expand Up @@ -149,11 +159,11 @@ class CanvasManager {
const index = this.canvas.getObjects().indexOf(this.originalImage);
this.canvas.remove(this.originalImage);
this.originalImage = fabricImg;
fabricImg._originalImage = true;
this._applyBackgroundImageProps(fabricImg);
this.canvas.insertAt(fabricImg, index >= 0 ? index : 0);
} else {
this.originalImage = fabricImg;
fabricImg._originalImage = true;
this._applyBackgroundImageProps(fabricImg);
this.canvas.insertAt(fabricImg, 0);
}
this.canvas.renderAll();
Expand All @@ -162,6 +172,29 @@ class CanvasManager {
});
}

/**
* 给作为背景的 fabric.Image 设置通用属性:
* - 标记为 _originalImage(序列化/恢复时识别)
* - 允许选中/接收事件(用于图层面板和调色工具定位背景图层)
* - 显示变换控制点,允许像普通图层一样拖拽移动、缩放、旋转
* @param {fabric.Image} fabricImg
*/
_applyBackgroundImageProps(fabricImg) {
fabricImg._originalImage = true;
fabricImg.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
lockMovementX: false,
lockMovementY: false,
lockRotation: false,
lockScalingX: false,
lockScalingY: false,
});
fabricImg.setCoords();
}

/**
* 获取画布当前图片 DataURL
* @param {object} [options] - 导出选项
Expand Down Expand Up @@ -197,6 +230,7 @@ class CanvasManager {
left: (cw - img.width * scale) / 2,
top: (ch - img.height * scale) / 2,
});
img.setCoords();
this.canvas.renderAll();
}

Expand Down Expand Up @@ -288,18 +322,28 @@ class CanvasManager {
'id',
'selectable',
'evented',
'hasControls',
'hasBorders',
'lockMovementX',
'lockMovementY',
'lockRotation',
'lockScalingX',
'lockScalingY',
'absolutePositioned',
'inverted',
'objectCaching',
'strokeLineCap',
'strokeLineJoin',
'_strokePosition',
'_layerName',
'_layerNameAuto',
'_layerBaseName',
'_layerKind',
'_layerShapeType',
'_layerColorPresetName',
'_layerWidthPresetName',
'_layerPresetName',
'_layerLocked',
'_mosaicDynamic',
'_mosaicMode',
'_mosaicSize',
Expand All @@ -326,31 +370,37 @@ class CanvasManager {
return;
}

// 提取 canvas 级别的 clipPath
// 提取 canvas 级别的 clipPath,避免修改历史栈里的原始快照对象。
const canvasClipPathData = json._canvasClipPath;
delete json._canvasClipPath;
const snapshot = { ...json };
delete snapshot._canvasClipPath;

this.canvas.loadFromJSON(snapshot, () => {
const finishRestore = () => {
// 恢复 originalImage 引用
const objs = this.canvas.getObjects();
this.originalImage = objs.find(o => o.type === 'image' && o._originalImage) || objs[0];
if (this.originalImage?.type === 'image') {
this._applyBackgroundImageProps(this.originalImage);
}
this.canvas.renderAll();
eventBus.emit('canvas:restored');
resolve();
};

this.canvas.loadFromJSON(json, () => {
// 恢复 canvas.clipPath
if (canvasClipPathData) {
fabric.util.enlivenObjects([canvasClipPathData], (objects) => {
this.canvas.clipPath = objects[0] || null;
if (this.canvas.clipPath) {
this.canvas.clipPath.absolutePositioned = true;
}
this.canvas.renderAll();
finishRestore();
});
} else {
// 快照中没有 clipPath → 清除画布上已有的(撤消裁切的关键)
this.canvas.clipPath = null;
finishRestore();
}

// 恢复 originalImage 引用
const objs = this.canvas.getObjects();
this.originalImage = objs.find(o => o.type === 'image' && o._originalImage) || objs[0];
this.canvas.renderAll();
eventBus.emit('canvas:restored');
resolve();
});
});
}
Expand Down Expand Up @@ -441,17 +491,16 @@ class CanvasManager {
});

// 窗口大小变化
window.addEventListener('resize', () => {
this._updateCanvasSize();
});
this._boundResize = () => this._updateCanvasSize();
window.addEventListener('resize', this._boundResize);

// 使用 ResizeObserver 监听容器变化
const container = this.canvas.wrapperEl?.parentElement;
if (container && window.ResizeObserver) {
const observer = new ResizeObserver(() => {
this._resizeObserver = new ResizeObserver(() => {
this._updateCanvasSize();
});
observer.observe(container);
this._resizeObserver.observe(container);
}
}
}
Expand Down
29 changes: 26 additions & 3 deletions plugins/Image-Toolbox/core/src/HistoryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class HistoryManager {
const json = this._cm.toJSON();
if (!json) return;

const last = this.undoStack[this.undoStack.length - 1];
if (last && this._isSameSnapshot(last, json)) return;

this.undoStack.push(json);

// 限制栈大小
Expand All @@ -46,14 +49,23 @@ class HistoryManager {

this._isRestoring = true;

// 保存当前状态到重做栈
const currentJson = this._cm.toJSON();
let prevJson = this.undoStack.pop();

while (prevJson && currentJson && this._isSameSnapshot(prevJson, currentJson) && this.undoStack.length > 0) {
prevJson = this.undoStack.pop();
}

if (!prevJson || (currentJson && this._isSameSnapshot(prevJson, currentJson))) {
this._isRestoring = false;
this._notify();
return;
}

if (currentJson) {
this.redoStack.push(currentJson);
}

// 恢复上一个状态
const prevJson = this.undoStack.pop();
try {
await this._restoreState(prevJson);
} catch (err) {
Expand Down Expand Up @@ -137,6 +149,17 @@ class HistoryManager {
undoCount: this.undoStack.length,
});
}

_isSameSnapshot(a, b) {
if (a === b) return true;
if (!a || !b) return false;

try {
return JSON.stringify(a) === JSON.stringify(b);
} catch (err) {
return false;
}
}
Comment on lines +153 to +162

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

_isSameSnapshot 中,代码使用 JSON.stringify(a) === JSON.stringify(b) 来对比两个画布快照是否相同。\n\n对于包含大图(如 base64 格式的背景图或高分辨率贴图)的画布,JSON.stringify 序列化会非常消耗 CPU 和内存,在频繁保存历史状态(如自由绘制、拖拽等)时会导致明显的界面卡顿。\n\n建议未来考虑引入轻量级的版本号/修改计数器(Revision Counter)机制,或者在对比时过滤掉图片数据源(src)等大体积的静态属性,以避免高频的深度序列化对比。

}

export default HistoryManager;
56 changes: 50 additions & 6 deletions plugins/Image-Toolbox/core/src/LayerManager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import eventBus from './EventBus.js';
import eventBus from './EventBus.js';

/**
* 图层管理器 — 管理 Fabric.js 物件的 z-order、显隐、锁定
Expand All @@ -7,6 +7,7 @@
class LayerManager {
constructor(canvasManager) {
this._cm = canvasManager;
this._cm.layerManager = this;
this._layers = []; // 图层元数据 [{ id, name, visible, locked, fabricObj }]
this._idCounter = 0;
}
Expand Down Expand Up @@ -43,19 +44,22 @@ class LayerManager {
meta.fabricObj = obj;
this._refreshMetaName(meta, obj, false, newLayers, currentObjects);
}
this._refreshMetaState(meta, obj, false);
meta.zIndex = objects.length - 1 - i;
newLayers.push(meta);
}

// 背景图层始终在列表末尾(面板最底部)
if (this._cm.originalImage) {
this._ensureBackgroundSelectable(this._cm.originalImage);
let bgMeta = oldLayers.find(l => l.fabricObj === this._cm.originalImage) || null;
if (!bgMeta) {
bgMeta = this._createMeta(this._cm.originalImage, true, newLayers, currentObjects);
} else {
bgMeta.fabricObj = this._cm.originalImage;
this._refreshMetaName(bgMeta, this._cm.originalImage, true, newLayers, currentObjects);
}
this._refreshMetaState(bgMeta, this._cm.originalImage, true);
bgMeta.zIndex = 0;
newLayers.push(bgMeta);
}
Expand Down Expand Up @@ -106,20 +110,55 @@ class LayerManager {
const meta = {
id,
name: nameInfo.name,
visible: obj.visible !== false,
locked: isBackground ? true : (!obj.selectable && !obj.evented),
visible: true,
locked: false,
fabricObj: obj,
zIndex: 0,
isBackground,
};

this._refreshMetaState(meta, obj, isBackground);

if (!isBackground) {
this._setObjectLayerName(obj, meta.name, nameInfo.auto, nameInfo.baseName);
}

return meta;
}

_refreshMetaState(meta, obj, isBackground = false) {
meta.visible = obj.visible !== false;
meta.locked = this._resolveLockedState(obj, isBackground);
meta.isBackground = !!isBackground;
}
Comment on lines +129 to +133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

BaseModule.js 中,代码调用了 layerManager?.getLayerByObject?.(obj)。然而,LayerManager 类中并没有定义 getLayerByObject 方法,这会导致该调用始终返回 undefined 并降级使用 obj._layerLocked。\n\n建议在 LayerManager 中显式实现 getLayerByObject 方法,以保证接口的完整性和设计的统一性。

  _refreshMetaState(meta, obj, isBackground = false) {
    meta.visible = obj.visible !== false;
    meta.locked = this._resolveLockedState(obj, isBackground);
    meta.isBackground = !!isBackground;
  }

  getLayerByObject(obj) {
    return this._layers.find(l => l.fabricObj === obj) || null;
  }


_resolveLockedState(obj, isBackground = false) {
if (isBackground) return true;
if (typeof obj?._layerLocked === 'boolean') return obj._layerLocked;

// 旧版本把工具激活期间创建的图层也标成 selectable/evented=false。
// 没有明确锁定标记时按未锁定处理,避免移动/框选工具无法选中这些图层。
return false;
}

_ensureBackgroundSelectable(obj) {
if (!obj) return;

obj._originalImage = true;
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
lockMovementX: false,
lockMovementY: false,
lockRotation: false,
lockScalingX: false,
lockScalingY: false,
});
obj.setCoords();
}

_refreshMetaName(meta, obj, isBackground = false, newLayers = null, currentObjects = null) {
const nameInfo = this._resolveLayerName(obj, isBackground, newLayers, meta, currentObjects);
meta.name = nameInfo.name;
Expand Down Expand Up @@ -378,6 +417,7 @@ class LayerManager {
if (!meta || meta.isBackground) return;

meta.locked = !!locked;
meta.fabricObj._layerLocked = meta.locked;
meta.fabricObj.set({
selectable: !meta.locked,
evented: !meta.locked,
Expand All @@ -398,9 +438,13 @@ class LayerManager {
const meta = this._layers.find(l => l.id === layerId);
if (!meta) return;

// 即使图层被锁定也触发事件(让橡皮擦等工具能响应图层切换),
// 但不调用 setActiveObject(避免误操作锁定图层)。
if (!meta.locked) {
// 即使图层被锁定也触发事件(让橡皮擦等工具能响应图层切换)。
// 普通锁定图层不调用 setActiveObject(避免误操作);
// 但背景图层允许选中和变换;图层锁定只限制删除、改名和排序。
if (!meta.locked || meta.isBackground) {
if (meta.isBackground) {
this._ensureBackgroundSelectable(meta.fabricObj);
}
this._cm.canvas.setActiveObject(meta.fabricObj);
this._cm.canvas.renderAll();
}
Expand Down
Loading
Loading