@Y!6Q-PP
zo-S$*c@e%vI*hq;yfXI7MbsM3n@GV6BtGA-()?uIo9g`gHcMJGhA{BC^iiLZ?EjJ%!QnZJG1dO(iSOp(3{=&V
z%MGGNB8gybTS`5wH(s2IzcD=CUoySMo4hmM?8I{ECGC{?*~k9>uMS1BUO&I995UNh{yp-lFYF?A!APn!b)?|gf*kO9(aHTs
zgz6AX5WiTM(B1x)o5xg&49x78toDO}p6!AJEP2sNK>skk@9eHT5m^=DJEf>-%1j}r
z`iAT`^+e@msrI(C2CtFc_LT&ogXx1XrG`Q2p`@fPk5<r6Mw3WWuy9-yba2LG#sUA$yA$x_b+ML086CrGvDDKydqP6^qmTR
zsJzbs`HF-WOMHiHnDKBP-Y}vpbObfJGz(S>T3mR}ErO}^k-kxSxTIez;Y!4}BtMF8
zi)LTN{7eUp$)MN?oi*8=FN;tTr
zhw}bWslIxG{~z|jwB4_}3Y*;bGmq{2{mly^eM0}|VX8a7y!>|N$&-H)KSS#E@XMNO
z`De5G52{DvnSYm#L}dR4-}hH1;TZ8aocshCEws=2=i3vgOmQZI3@ZZ!0V@sj_oQxn
zv`o3&r_fyAmK%T?81c=93s;vbm#u!3x(6niZ)S&z_tV9Nj=9%v-6MzoHfHrA8QfsIu0@=RPp*g__WB
zD7-nl|E;`hOIKI-H=4D}(&p2?b!T;5<=^hl-x56p@Zs#?=sA67IBfRfrECLx<%*H*
zH3J?Q8e0f6l4i;t3wh5nmZljf?+n8}K
zra3Zzg;$TX$_Ko!8!^#Q9}qwSPzbt}@JpzH(xc1Ucb5P>)(S&}DK1O)Pa#Rl=qu<_
z#yJ#Qb4s(b)bsqsEkQk|Wv9itLyWsA
z+gJ+|EAP?8Yh&WH43Y+=y~SuWG1|D}TB9a<^@!7*5H7_;YmKpSni%ieOfO@wIuOGH
z@!+i>4-<$5tG5OrPVKKor{0+8m1)#QE8=uON20r(^>GNuP_cR{2A)-07=I$hY120!
zP2UoyiPhX192wa>!c&L;yJ{I2(^A1GO~F~`KHzbl_&xbSz_8TTkLbqtmF%7Nx=
zxiMZVJVOZLqn6;_RhjXN?Dt9xGDLG9F7O#hY{SY_?F9}z5+(4#yX%pmbV}?PU5Dl6
zEc&5vwxTbWU&!#`1@bTor<*_^k|<=El8OY7LurhA^{W$iJhpr)IQ|mj=@mZSi)U_L
z@iA!6!XsFz9ic1zh
zsBi3KCdXIKNbv{cAxpw6G$>u238QyA370OU8bcQozH%Empklg&GktJHn<=yk*p
znRr3isx}go$pA1e*U&zkddp|5*R+pbdm9cqr~;#~aSHc|joglT)?0hMK2G!U#0SFr
z>CB54naB`-h-Qhyhk4{e8FAX?`hso7;Kn~ys)ERdB^^->u&DDFfBpq)DEoZ>nhmC;
z-=YQBm-$6~SoT4k!ZSLf=8qKnyQZ@DJm8rzFGXsRiB+c`TrD2vtFSkFY+f(>+@yc<
za7YNKe4n)fW3TpW!Bd{bCeQBo{EtkQwK(mp;Qs-Tj%i-+`4EBRx@T9H7<%T<{s6O8
z)hY!*9(?OwGM?!s7v%tli91gBRuc1HL@P<#%!_=O<|6@-o5NU+)B|M#EnWaYt$J5sU0KD{Xi3b-92D%&1F)
z$khHX%xo8_|EfriXC)Jn2T^g5&Di*G&Wf#9jd4Ib~n2;i)9?gyWv?Ch=092ow>k0C)KA&fqgew>-EEfC@ZR_8>VGDoG!Nq2Nw^YBPY&WxDpT&
z5tERTkyB7o!Qj+^l#~>(<~ES_nEa%0m9aq(ZGol)X+dY?BW}1zO($@sD^3D&EpLB@
zS;#`vHnIe4D{;H4K#OmlbuKX!SZRYKyCmD_jH_vn!P0alQ{{MA)dx%sRhOK6D7v&L
zRO14t7NHvV#MhK6OW8(TX)}3&FDUs+illD7<}=(7b0^=>sN>TbLmKQfTyz}8*pb4Q
z1(GPy{S$cPdC#zB1KQIu16Ne7h7EKiu||9q;ZSon8_tq
z$#B1+lsr7H{pU$C?-Q|Fq9RyCRaJ#347mzNpv#i%-bJBGGd#b|9)bh;eA=SY0G!0M
z!AdN^I_Atqm*xWPRjX@UrQQy75vj{^SLM7p^`+pO6j~?S>450;32y^iPMZO-FVa=M
z%5su!9B`ZwKp@O7r7==j@{&H0C0BlREC5hx87=~Blku*p6?1?f
zXTG^to9h5E!`t|Ic2i9@e%ivjIgLBfoCfBcaWXWAd4+_%wij66z}fO+{Th5saCh
zq@pDDVSubrfOYa&rH`=kRI7^w6?52WmIAS_ROgC1qvB-jz}t3Mn!xiMRAnJ
zf>;u6oQ`uBmS3EG@z&*MuIAJ=AzC&1D%ycX(Ck^M9(=OrkRwiLRYC_2|BrY;k1oTe
zw3?Om8PKWOgc)P{b!#%DS2AhNpmA;56&+ekYZx`6A~gXRy#vxGLg5_2fbH8J(kVGPdLY6pE2^cUPM(7FyAIg{Si#uCW
z%uY+E2^ptNp4gYqk3d>XCpiLBgYVbU<1`Hl&KMf$d=3ulpM7H1^tpYRVd*Uvi#l!-
zBv>Fq7*C0iw=j)G#nO|p06_sv^49T?hA?xUct0^a+wIa#&d-}V9V-p`tW|4`2&tVjax=z)QVYfbD
z3yJBbh218IuqIBsEg0?1m0+8O2$}MxxUEymc5>n&NU%qExch_$^zkuS&y5F;mu`Y%
zlR9>$80Le;6Nw{^bZ
zV_eEx$jzT5f!v5G=1^Ker*#Gpt1F<;VqBWmrqpI=H4ZZcV5nihhjMYMvXQ
zK+Qzq)Ntw0s59e=JT~kYYmNXHx4b(oKGrb^DeB-Li8tFS88UHZAfOrNp$I^Xs++--
z8W_9AJgWoaLezp#jc>ZwPZm|eDk)I58&vh6WNKdRsOOAQ3hMe`Y{i)cSq&?+ciQk9
zTjBdIXRL4`BD*H2`w>jSskx1sO*u$h%Zkj8@ML*cI(IV4WErU_?@7-kLo-
zEU!+tmXaq@cM`iElc?C@IL>zcqaX#>8+144n;qmzNC2?w?^>*$DixFwmtmY=!CChb
zh_G^PXm;uK_i2{5S3!(jdv~xdj&(Y2uUBtYL3FzfC=70`e@EMFn@sag%?7zSFFbd=
zGAoglKQeRV*et(NB-JAI`j*+xxPkRFP5
z5}Q3JXC(QHFFj$Op6%SiAO^BRoS{A06pS!YPzJ=w1`Lj48OsZ65gU$cb;gr1v&}^&
zX!j?H+r3RL^L1Lh)(bNkJge&+6(;FY_^@k3Gn6i6r)x2lMOmB?aTTGBWGbz_s@c8L
zo^7lbGqf`lp6AUo2>F#2=c2g-YcV=0mF>UMMA)!;e@GEtjxT})+l4f}<
z*edQ%eawGty?OQM+}Y{!?w?cy=f;ZhkQ>tBk5?^ick)W>KCZx-VR`1Ge1*hhiG*GW
zr;5213c0T8TaT_oc;#R!%a@3p{5Wp&)->1GmC=}T^*0Gv`AAC~sbkg5T)kGz-d$ViyHPeu%${>Wy#a5lxQE`IL<;r@-ROzd5TWclZ%UK!DS-vV$;DG
z;TG6v*8jJjjT!JHxr;bSQbOsngK}m>*48sVaR%V*Om#*~k7>&7dtK5Y1cZX@Tb;f1
zYn<{l)U=Qt1llt<>=$KstzTDS-K}d4?qy`98N&3EkQf72=Pm!AVxf>I!=h2j%POkf
zEY$W^$_VwdkuoY8q4ea8M5kK36m)s~aH>T#O!XdG?`Yx4rwA-?4uLB2&Lzh_YLfpV
zyPBGsnOZ&p&EKYJ(SVE{9rTWiL=McmNtq$SndxTN+ue`Oos`s)^+ISpZJiL;K}XPN
z4g;7dO&>;^#vLfm1e$5)O+=1O4pY#Ouu|LGE2HB>u@4PGWQhWpexoj8pG)F
z9aB9Ucpn}J19TF=NHvHK)e&FGBFyCBN8PMp%
z6Em4)Ch3jR$T!$+*#iYMWNzY}HK
zrN3))D|53`GZ{vvj+4QQEe;FMNo6N8(-~=WL&C!9_Udo>fL*O5v;wO>&5&4U>MxB|
z!8E}Etf+}Zw$^u(pPOB9qFxr~()4BWU8_x$(dv)uUz(aWf4=_53)1J4^8BDcvd6q%
z@BF*TWQY@DVNnzAyQ|dyo?GBu-sRpL5a!^DagQkSZQPl2>HS;8TmLC)sCoPr_;~FwcXWFC6tRF)Ks@pP
zf5V`VGa4Lxg@hv7Nj<`?{6Z`w=1=L_`lSBt9I
zw}V)r(ACVV;BYmJ9zpB$y>a3?h3OM3+C!zx$WWFBirc1g1h)t`XB|DUM+ET3?`&J#_{`9~o5>Iu}_h`#CcJ;Mc!
zg;wk1>AR2L+fJ;?^qKiHMPJxBZ{APVCr^@7mH~Z)eD}(0d1rnYJM8Gsod0L{RJ8GT
zaI|@TU~xb}K|pcfuOOp^_IdyMdIkXpIbYGQp=tURi}&fbKI-B3stR)J+H*9J0#0gC
zoWIh`?^*FkzYqB3-ZVxq58{{wjyczUcdT*x{aTgA5RbFL%yK4tkU5ui5I0LMso|Et
z0%#IjSqpGG_m*dLoS|Q_j
z0H8qvJlYT+oP-A8dkmWUAEW_~RrGMW&R}V3U6#U+blM=_D?wXYbS?TyH>>F#0SDQl
zZH(e9QBTj*EV1z>$Y^c4C^Cv~X80}An)3bU`1;QA&;OVxQKEg`A7A6tqtctD(!^{b
zM`nBWU`H<_-W^1_k7{L!%P&!;9Zi(SNt9@1Mnj@IhC{OlWOZFO5*)Xv%R!o;t-)IL=?6KJNDx;L
zql?$3cx7v)af}2Rkde4%=Y0|?X6eFWx)7$#+CjU=?&c&j0-KI!ZcdaWNNxih8`#vt
zvj_jn#AZ9Yi3`G*sj{+la5fSq!FE}2$9B63tCCXR=jBfE*=7X7{9R7S&9n20-wSu#
zz4P_GqXaPMW*UiRW-z8JmOFRbmnqqp;oL$%7=dp8meCI%b<^+p_?0<6^_cLu=5ck}
z{w1jE6n2VU!3Sc7=uq*>4YM=gy1U!}#reM@t?sc*%nS_WmA!+r>BcN6-HHgMLZ-0M
z<=IKgrYE;kLKSi3hf7rBDeb~iK}Wtl1&I!M@a}p{1P+}rL$=2Baslp06p?4kp%%a@
zN(e=WCJiAFc1Sde!^dDXs0au6Ufqh6?T<~L@=v}LQsg|9vUC2XzK@~%mL6OAt=x6@
z3UM()?KWGOwvz6dt}!#pzjNd38rFn^Me4}#dQ*7B0G>=E;|G)mg~CuQ8EYOyS}6!20XM?(W?BuI6zJ#FcQ(uFeO=tphNR#)AhU>xq?a
zA^U(huc_59dM<~j5NkEq-*T044%YsLeGWgi?7&{Z9H@h?V-+ze=jm-?ErOp00PxOH
zbWG}Rsb+emfArd4f7o6S5-arPx%)Pd*EE}N?f3ea?&T>Oj~c__FI>Q5C#Vw+Ca=aM
zg`7_!%AH%6zf~L7@TWkKA5-78Ce{Sh)3xN^4?6yFIZ=CmrpOcbR=>n;s(gU$!$UD*mmHrdk+?|GJ*KR50od*QQt=PN^lHMsr
zu(PvwsH26DmiL6AByvQSp2mx>(P3cWQoiUVvl1%!TwF;PhSqvf1iwScsbMZwi-pQ^
zNu|##=K=)S>UvAa_18elPe6o{d
zMb&h}v~0)qd`QSBs2n+Q=E4;X9Rm{!8wVE;pCF(g0NN3_rp`%Q94kvjVtk+v?-=Yb
z>i&X(NrG)l>)Bx0qIdf+)VV!R-qA$pm!{Gi=VhzTpev~L?wvNV
z9ZO`&u}NrHXl+^&Xh$tcL0asWp|arL$mtfxl3XMAFF2+%oi1qIaJe?%vN^vcimXjI
z`u7NqmbBX^k*E#RGL#18e&a!idYcW)+WG116{*ImPU~%(Wu4q_@+{ai-{U*Dt^!w-
en%E)P4s-y?D-eTU%K_1SWFSZLv+S)>0RRB`l_|9V
diff --git a/src/component/Canvas/CanvasEditor/commands/FillGroupLayerBackgroundCommand.js b/src/component/Canvas/CanvasEditor/commands/FillGroupLayerBackgroundCommand.js
index 98eb683a..cc773d34 100644
--- a/src/component/Canvas/CanvasEditor/commands/FillGroupLayerBackgroundCommand.js
+++ b/src/component/Canvas/CanvasEditor/commands/FillGroupLayerBackgroundCommand.js
@@ -69,7 +69,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
layer.clippingMask,
this.canvas
);
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.newFill = new fabric.Rect({
width: clippingMaskFabricObject.width,
@@ -117,7 +117,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
this.parent.clippingMask,
this.canvas
);
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.newFill = new fabric.Rect({
width: clippingMaskFabricObject.width,
@@ -222,7 +222,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
this.parent?.clippingMask,
this.canvas
);
- clipPath.clipPath = null;
+ // clipPath.clipPath = null;
clipPath.set({ absolutePositioned: true });
this.group.clipPath = clipPath;
}
diff --git a/src/component/Canvas/CanvasEditor/commands/FillLayerBackgroundCommand.js b/src/component/Canvas/CanvasEditor/commands/FillLayerBackgroundCommand.js
index 2ff386fd..9f1daeb2 100644
--- a/src/component/Canvas/CanvasEditor/commands/FillLayerBackgroundCommand.js
+++ b/src/component/Canvas/CanvasEditor/commands/FillLayerBackgroundCommand.js
@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
layer.clippingMask,
this.canvas
);
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位
diff --git a/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
new file mode 100644
index 00000000..617c4ea4
--- /dev/null
+++ b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
@@ -0,0 +1,301 @@
+import { Command } from "./Command";
+import { findLayerRecursively } from "../utils/layerHelper";
+import { fabric } from "fabric-with-all";
+import {
+ findObjectById,
+ generateId,
+ insertObjectAtZIndex,
+ removeCanvasObjectByObject,
+ createPatternTransform,
+} from "../utils/helper";
+import { restoreFabricObject } from "../utils/objectHelper";
+
+const scale = 0.3;// 默认缩放比例
+
+export const FillSourceToBase64 = (source) => {
+ if (source?.toDataURL) {
+ return source.toDataURL?.();
+ } else if (source?.src) {
+ return source.src;
+ }
+ return source;
+}
+
+/**
+ * 填充图案平铺命令
+ * 填充重复属性:repeat | repeat-x | repeat-y | no-repeat
+ * 默认缩放比例:0.3
+ * 默认偏移量:50%
+ */
+export class FillRepeatCommand extends Command {
+ constructor(options) {
+ super({ name: "填充图案平铺", saveState: true });
+ this.canvas = options.canvas;
+ this.layers = options.layers;
+ this.canvasManager = options.canvasManager;
+ this.layerManager = options.layerManager;
+ this.layerId = options.layerId;
+ this.fillRepeat = options.fillRepeat;
+ this.oldObjects = null;
+ this.oldLocked = null;
+ this.oldIsDisableUnlock = null;
+ }
+
+ async execute() {
+ const { layer } = findLayerRecursively(this.layers.value, this.layerId);
+ if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
+ console.warn("图层不存在或没有 fabric 对象");
+ return false;
+ }
+ const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
+ if (!object || (object.type !== "rect" && object.type !== "image")) {
+ console.warn("当前对象不能平铺", object.type);
+ return false;
+ }
+ this.oldObjects = object;
+ const img = await new Promise((resolve, reject) => {
+ if (object.type === "rect") {
+ let source = object.fill.source;
+ resolve(source);
+ } else if (object.type === "image") {
+ // resolve(object.getElement());
+ // fabric.Image.fromURL(
+ // object.src,
+ // v => resolve(v),
+ // { crossOrigin: "anonymous" }
+ // );
+ const imgElement = object.getElement();
+ // 创建透明 Canvas
+ const tcanvas = document.createElement('canvas');
+ tcanvas.width = imgElement.width;
+ tcanvas.height = imgElement.height;
+ const ctx = tcanvas.getContext('2d');
+ ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
+ ctx.drawImage(imgElement, 0, 0);
+ resolve(tcanvas);
+ }
+ });
+ const fill_ = {
+ source: FillSourceToBase64(img),
+ gapX: 0,
+ gapY: 0,
+ };
+ const bgObject = this.canvasManager.getBackgroundLayerObject();
+ const pattern = new fabric.Pattern({
+ source: img,
+ repeat: this.fillRepeat,
+ patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
+ offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
+ offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
+ });
+ const rect = new fabric.Rect({
+ id: object.id,
+ layerId: object.layerId,
+ layerName: object.layerName,
+ fill_,
+ });
+ layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
+ this.oldLocked = layer.locked;
+ // this.oldIsDisableUnlock = layer.isDisableUnlock;
+ // layer.isDisableUnlock = true;
+ if (this.oldObjects.type === "rect") {
+ rect.set({
+ width: object.width,
+ height: object.height,
+ top: object.top,
+ left: object.left,
+ originX: object.originX,
+ originY: object.originY,
+ angle: object.angle,
+ scaleX: object.scaleX,
+ scaleY: object.scaleY,
+ flipX: object.flipX,
+ flipY: object.flipY,
+ });
+ } else {
+ rect.set({
+ width: bgObject.width,
+ height: bgObject.height,
+ top: bgObject.top,
+ left: bgObject.left,
+ originX: bgObject.originX,
+ originY: bgObject.originY,
+ });
+ layer.locked = true;
+ }
+ rect.set("fill", pattern);
+ this.canvas.add(rect);
+ this.canvas.remove(object);
+ await this.layerManager?.updateLayersObjectsInteractivity();
+ await this.layerManager?.sortLayersWithTool?.();
+ await this.canvasManager.thumbnailManager?.generateLayerThumbnail(
+ this.layerId
+ );
+ await this.layerManager.selectLayerObjects(this.layerId);
+ return true;
+ }
+
+ async undo() {
+ if (!this.oldObjects) {
+ console.warn("没有旧对象可恢复");
+ return false;
+ }
+ const { layer } = findLayerRecursively(this.layers.value, this.oldObjects.layerId);
+ if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
+ console.warn("图层不存在或没有 fabric 对象");
+ return false;
+ }
+ const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
+ this.canvas.remove(object);
+ this.canvas.add(this.oldObjects);
+ layer.fabricObjects = [this.oldObjects.toObject(["id", "layerId", "layerName"])];
+ layer.locked = this.oldLocked;
+ // layer.isDisableUnlock = this.oldIsDisableUnlock;
+ await this.layerManager?.updateLayersObjectsInteractivity();
+ await this.layerManager?.sortLayersWithTool?.();
+ this.canvas.renderAll();
+ this.canvasManager.thumbnailManager?.generateLayerThumbnail(this.layerId);
+ return true;
+ }
+}
+
+
+/**
+ * 填充图案更改参数
+ */
+export class FillRepeatChangeCommand extends Command {
+ constructor(options) {
+ super({ name: "填充图案更改参数", saveState: true });
+ this.canvas = options.canvas;
+ this.layers = options.layers;
+ this.canvasManager = options.canvasManager;
+ this.layerManager = options.layerManager;
+ this.layerId = options.layerId;
+ this.newPattern = options.newPattern;
+ this.oldPattern = null;
+ }
+
+ async execute() {
+ const { layer } = findLayerRecursively(this.layers.value, this.layerId);
+ if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
+ console.warn("图层不存在或没有 fabric 对象");
+ return false;
+ }
+ const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
+ if (!object || object.type !== "rect") {
+ console.warn("当前对象不是矩形", object);
+ return false;
+ }
+ this.oldPattern = object.oldPattern || object.get("fill");
+ delete object.oldPattern;
+ const oldPattern = { ...this.oldPattern };
+ delete oldPattern.id;
+ const pattern = new fabric.Pattern({
+ ...oldPattern,
+ ...this.newPattern,
+ });
+ object.set("fill", pattern);
+ this.canvas.renderAll();
+ return true;
+ }
+
+ async undo() {
+ if (!this.oldPattern) {
+ console.warn("没有旧图案可恢复");
+ return false;
+ }
+ const { layer } = findLayerRecursively(this.layers.value, this.layerId);
+ if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
+ console.warn("图层不存在或没有 fabric 对象");
+ return false;
+ }
+ const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
+ if (!object || object.type !== "rect") {
+ console.warn("当前对象不是矩形", object);
+ return false;
+ }
+ const pattern = new fabric.Pattern({
+ ...this.oldPattern
+ });
+ object.set("fill", pattern);
+ this.canvas.renderAll();
+ return true;
+ }
+}
+
+/**
+ * 填充图案更改间隙
+ */
+export class FillRepeatGapChangeCommand extends Command {
+ constructor(options) {
+ super({ name: "填充图案更改间隙", saveState: true });
+ this.canvas = options.canvas;
+ this.layers = options.layers;
+ this.canvasManager = options.canvasManager;
+ this.layerManager = options.layerManager;
+ this.layerId = options.layerId;
+ this.newGapX = options.newGapX;
+ this.newGapY = options.newGapY;
+ this.record = !!options.record;
+ this.oldGapX = null;
+ this.oldGapY = null;
+ }
+
+ async execute(isUndo = false) {
+ const { layer } = findLayerRecursively(this.layers.value, this.layerId);
+ if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
+ console.warn("图层不存在或没有 fabric 对象");
+ return false;
+ }
+ const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
+ if (!object || object.type !== "rect") {
+ console.warn("当前对象不是矩形", object);
+ return false;
+ }
+ if (!object.fill_) {
+ object.fill_ = {
+ source: FillSourceToBase64(object.fill.source),
+ gapX: 0,
+ gapY: 0,
+ }
+ }
+ if (isUndo) {
+ object.fill_.gapX = this.oldGapX;
+ object.fill_.gapY = this.oldGapY;
+ } else {
+ if (!object.oldFill_ && this.record) {
+ object.oldFill_ = { ...object.fill_ };
+ }
+ this.oldGapX = object.fill_.gapX;
+ this.oldGapY = object.fill_.gapY;
+ object.fill_.gapX = this.newGapX;
+ object.fill_.gapY = this.newGapY;
+ }
+ const image = new Image();
+ image.src = object.fill_.source;
+ await image.decode();
+ // 创建透明 Canvas
+ const tcanvas = document.createElement('canvas');
+ tcanvas.width = image.width + object.fill_.gapX;
+ tcanvas.height = image.height + object.fill_.gapY;
+ const ctx = tcanvas.getContext('2d');
+ ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
+ ctx.drawImage(image, 0, 0);
+
+ const fill = object.get("fill");
+ fill.source = tcanvas;
+ object.set("fill", new fabric.Pattern(fill));
+ this.canvas.renderAll();
+ return true;
+ }
+
+ async undo() {
+ if (this.oldGapX === null || this.oldGapY === null) {
+ console.warn("没有旧间隙可恢复");
+ return false;
+ }
+ await this.execute(true);
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js
index cec13748..524b2e37 100644
--- a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js
+++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js
@@ -10,6 +10,7 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
import { ToolCommand } from "./ToolCommands";
import {
findObjectById,
+ findObjectByLayerId,
generateId,
getObjectZIndex,
insertObjectAtZIndex,
@@ -19,7 +20,7 @@ import {
} from "../utils/helper";
import { fabric } from "fabric-with-all";
import { restoreFabricObject } from "../utils/objectHelper";
-
+import EventManager from "../utils/event.js";
/**
* 添加图层命令
*/
@@ -36,7 +37,7 @@ export class AddLayerCommand extends Command {
this.insertIndex = options.insertIndex;
this.oldActiveLayerId = null;
- this.beforeLayers = [...this.layers.value]; // 备份原图层列表
+ this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
this.options = options.options || {};
}
@@ -70,7 +71,7 @@ export class AddLayerCommand extends Command {
undo() {
// 从图层列表删除该图层
- this.layers.value = [...this.beforeLayers];
+ this.layers.value = JSON.parse(this.beforeLayers);
// 恢复原活动图层
this.activeLayerId.value = this.oldActiveLayerId;
@@ -251,12 +252,12 @@ export class PasteLayerCommand extends Command {
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
null;
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
- clippingMaskFabricObject.dirty = true;
+ // clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
// 添加所有对象到画布
allObjects.forEach((obj) => {
@@ -802,15 +803,23 @@ export class ToggleLayerVisibilityCommand extends Command {
// 切换可见性
this.layer.visible = !this.layer.visible;
+ const ids = [this.layerId];
+ const childLayers = this.layer?.children || [];
+ childLayers.forEach((childLayer) => {
+ childLayer.visible = this.layer.visible;
+ ids.push(childLayer.id);
+ });
// 更新画布上图层对象的可见性
if (this.canvas) {
- const layerObjects = this.canvas
- .getObjects()
- .filter((obj) => obj.layerId === this.layerId);
- layerObjects.forEach((obj) => {
- obj.visible = this.layer.visible;
- });
+ this.canvas.getObjects().forEach((obj) => {
+ if (ids.includes(obj.layerId)) {
+ obj.getObjects?.()?.forEach((item) => {
+ item.visible = this.layer.visible;
+ });
+ obj.visible = this.layer.visible;
+ }
+ });
}
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity();
@@ -868,13 +877,14 @@ export class ToggleChildLayerVisibilityCommand extends Command {
// 更新画布上图层对象的可见性
if (this.canvas) {
- const layerObjects = this.canvas
- .getObjects()
- .filter((obj) => obj.layerId === this.layerId);
-
- layerObjects.forEach((obj) => {
- obj.visible = this.childLayer.visible;
- });
+ this.canvas.getObjects().forEach((obj) => {
+ if (obj.layerId === this.layerId) {
+ obj.getObjects?.()?.forEach((item) => {
+ item.visible = this.childLayer.visible;
+ });
+ obj.visible = this.childLayer.visible;
+ }
+ });
}
// 更新画布上对象的可选择状态
@@ -1007,9 +1017,8 @@ export class LayerLockCommand extends Command {
// 如果是组图层,递归更新所有子图层
if (
- layer.type === "group" &&
layer.children &&
- Array.isArray(layer.children)
+ Array.isArray(layer.children) && layer.children.length > 0
) {
layer.children.forEach((child) => {
this._updateLayerLockState(child, locked);
@@ -1108,7 +1117,7 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
-
+ EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
return true;
}
@@ -1130,6 +1139,7 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
}
+ EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
}
getInfo() {
@@ -1371,7 +1381,7 @@ export class GroupLayersCommand extends Command {
// 备份原图层
this.originalLayers = [...this.layers.value];
// 新组ID
- this.groupId =
+ this.groupId = options.id ||
generateId("group_layer_") ||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
@@ -4434,3 +4444,87 @@ export class ChildLayerLockCommand extends Command {
};
}
}
+/**
+ * 设置图层混合模式
+ */
+export class SetLayerCompositeCommand extends Command {
+ constructor(options) {
+ super({
+ name: "设置图层混合模式",
+ saveState: false,
+ });
+ this.canvas = options.canvas;
+ this.layers = options.layers;
+ this.layerManager = options.layerManager;
+ this.layerId = options.layerId;
+ this.newValue = options.newValue;
+ this.oldValue = options.oldValue;
+ }
+
+ execute(isUndo = false) {
+ const { layer } = findLayerRecursively(this.layers.value, this.layerId);
+ const { object } = findObjectByLayerId(this.canvas, this.layerId);
+ if (!layer || !object) {
+ console.error(`图层${this.layerId}不存在`);
+ return false;
+ }
+ // console.log("==========", this.newValue, this.oldValue);
+ const value = isUndo ? this.oldValue : this.newValue;
+ layer.blendMode = value;
+ object.set("globalCompositeOperation", value);
+ this.canvas.renderAll();
+ const event = isUndo ? "object:composite:undo" : "object:composite:execute";
+ EventManager.emit(event, object);
+ return true;
+ }
+
+ undo() {
+ return this.execute(true);
+ }
+}
+
+
+/**
+ * 设置颜色图层颜色
+ */
+export class SetColorLayerFillCommand extends Command {
+ constructor(options) {
+ super({
+ name: "设置颜色图层颜色",
+ saveState: false,
+ });
+ this.canvas = options.canvas;
+ this.layerManager = options.layerManager;
+ this.object = options.object;
+ this.layer = this.layerManager?.getLayerById(this.object.layerId);
+ this.newFill = options.newFill;
+ this.oldFill = JSON.parse(JSON.stringify(this.object.fill));
+ }
+
+ async execute(isUndo = false) {
+ if (!this.object) {
+ console.error(`颜色图层不存在`);
+ return false;
+ }
+ const isVisible = this.layer?.visible;
+ if(!isVisible && this.layer) this.layer.visible = true;
+ const gradient = new fabric.Gradient({
+ type: "linear",
+ gradientUnits: "percentage",
+ ...(isUndo ? this.oldFill : this.newFill),
+ });
+ this.object.setFill(gradient);
+ this.canvas.renderAll();
+ await this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
+ this.object.id
+ );
+ if(!isVisible && this.layer) this.layer.visible = false;
+ this.layerManager?.updateLayersObjectsInteractivity();
+ return true;
+ }
+
+ undo() {
+ this.execute(true);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/component/Canvas/CanvasEditor/commands/ObjectCommands.js b/src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
new file mode 100644
index 00000000..a8e5c33a
--- /dev/null
+++ b/src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
@@ -0,0 +1,50 @@
+import { Command } from "./Command.js";
+
+/**
+ * 对象移动命令
+ * 轻量级命令,只记录对象的移动属性变化(位置)
+ */
+export class ObjectMoveCommand extends Command {
+ constructor(options) {
+ super({
+ name: options.name || "对象移动",
+ description: options.description || "移动对象",
+ saveState: false, // 自己管理状态,避免递归
+ });
+
+ this.canvas = options.canvas;
+ this.initPos = options.initPos;
+ this.finalPos = options.finalPos;
+ }
+
+ /**
+ * 执行命令
+ */
+ async execute() {
+ this.setObjectsPos(this.finalPos);
+ return true;
+ }
+ /**
+ * 撤销命令
+ * 应用初始状态
+ */
+ async undo() {
+ this.setObjectsPos(this.initPos);
+ return true;
+ }
+
+ async setObjectsPos(pos) {
+ const objects = this.canvas.getObjects();
+ const arr = typeof pos === "object" ? [pos] : pos;
+ arr.forEach((item) => {
+ const obj = objects.find((o) => o.id === item.id);
+ if(obj) {
+ obj.set({
+ left: item.left,
+ top: item.top,
+ });
+ }
+ });
+ this.canvas.renderAll();
+ }
+}
diff --git a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js
index 1243295a..fdf72140 100644
--- a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js
+++ b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js
@@ -234,7 +234,7 @@ export class AddObjectToLayerCommand extends Command {
parent.clippingMask,
this.canvas
);
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.fabricObject.clipPath = clippingMaskFabricObject;
// 标记为脏对象
diff --git a/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js b/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js
index 54c6285b..e4cca18e 100644
--- a/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js
+++ b/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js
@@ -46,13 +46,13 @@ export class RasterizeLayerCommand extends Command {
this.layerId
);
this.layer = layer;
- this.parentLayer = parent;
+ // this.parentLayer = parent;
- // 新增:如果有父图层,则栅格化父图层及其所有子图层
- if (this.parentLayer) {
- this.layer = this.parentLayer;
- this.layerId = this.parentLayer.id;
- }
+ // // 新增:如果有父图层,则栅格化父图层及其所有子图层
+ // if (this.parentLayer) {
+ // this.layer = this.parentLayer;
+ // this.layerId = this.parentLayer.id;
+ // }
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
@@ -153,7 +153,7 @@ export class RasterizeLayerCommand extends Command {
});
// 恢复原始图层结构
- this.layers.value = [...this.originalLayerStructure];
+ this.layers.value = JSON.parse(this.originalLayerStructure);
// 恢复原活动图层
this.activeLayerId.value = this.layerId;
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
*/
_saveOriginalLayerStructure() {
// 只保存相关的图层结构,而不是整个图层数组
- this.originalLayerStructure = JSON.parse(JSON.stringify(this.layers.value));
+ this.originalLayerStructure = JSON.stringify(this.layers.value);
}
/**
@@ -517,12 +517,12 @@ export class ExportLayerToImageCommand extends Command {
this.canvas
);
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
- clippingMaskFabricObject.dirty = true;
+ // clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
}
diff --git a/src/component/Canvas/CanvasEditor/commands/StateCommands.js b/src/component/Canvas/CanvasEditor/commands/StateCommands.js
index 03ba2b41..40db92ac 100644
--- a/src/component/Canvas/CanvasEditor/commands/StateCommands.js
+++ b/src/component/Canvas/CanvasEditor/commands/StateCommands.js
@@ -2,6 +2,7 @@ import { findObjectById } from "../utils/helper";
import { findLayerRecursively } from "../utils/layerHelper";
import { restoreFabricObject } from "../utils/objectHelper";
import { Command } from "./Command";
+import EventManager from "../utils/event.js";
/**
* 对象变换命令
@@ -75,7 +76,7 @@ export class TransformCommand extends Command {
// 触发画布更新
this.canvas.renderAll();
-
+ EventManager.emit("object:modified:execute", targetObject);
return true;
}
@@ -113,7 +114,7 @@ export class TransformCommand extends Command {
}, 300);
// 触发画布更新
this.canvas.renderAll();
-
+ EventManager.emit("object:modified:undo", targetObject);
return true;
}
@@ -167,7 +168,7 @@ export class TransformCommand extends Command {
);
if (clippingMaskFabricObject) {
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
diff --git a/src/component/Canvas/CanvasEditor/commands/UpdateGroupMaskPositionCommand.js b/src/component/Canvas/CanvasEditor/commands/UpdateGroupMaskPositionCommand.js
index cf6114a3..cd114d6c 100644
--- a/src/component/Canvas/CanvasEditor/commands/UpdateGroupMaskPositionCommand.js
+++ b/src/component/Canvas/CanvasEditor/commands/UpdateGroupMaskPositionCommand.js
@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
return;
}
- clippingMaskFabricObject.clipPath = null;
+ // clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
diff --git a/src/component/Canvas/CanvasEditor/components/CropImage.vue b/src/component/Canvas/CanvasEditor/components/CropImage.vue
index 6eb5d7b7..eb49c6a7 100644
--- a/src/component/Canvas/CanvasEditor/components/CropImage.vue
+++ b/src/component/Canvas/CanvasEditor/components/CropImage.vue
@@ -1,5 +1,4 @@
-
+
@@ -1298,9 +1298,11 @@ defineExpose({
-
-