合并画布代码
1601
package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"core-js": "^3.8.3",
|
||||
"driver.js": "^1.3.1",
|
||||
"echarts": "^5.5.1",
|
||||
"fabric-with-all": "^5.3.1",
|
||||
"element-plus": "^2.4.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fingerprintjs2": "^2.1.4",
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
<body>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> -->
|
||||
<script src="/js/color-thief.js"></script>
|
||||
<script src="/js/fabric.min.js"></script>
|
||||
<script src="/js/fabric.brushes.js"></script>
|
||||
<script src="/js/aligning_guidelines.js"></script>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
|
||||
1
public/js/fabric.min.js
vendored
1
src/assets/icons/CBottom.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089605497" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22868" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M185.396221 1024a49.219161 49.219161 0 0 1 0-98.462134h630.08622a49.219161 49.219161 0 0 1 0 98.462134z m273.146103-175.898375L141.772852 518.402301a49.147725 49.147725 0 1 1 70.84035-68.149608l232.689715 242.000161a0.142871 0.142871 0 0 0 0.142872-0.142871V50.332128a49.83827 49.83827 0 0 1 52.409953-50.243072 49.195349 49.195349 0 0 1 46.242675 49.100102v641.111122c0 0.142871 0 0.309554 0.142872 0.142871l232.713527-241.976349a49.147725 49.147725 0 1 1 70.84035 68.149608L529.382674 848.101625a48.981042 48.981042 0 0 1-70.84035 0z" fill="#040000" p-id="22869"></path></svg>
|
||||
|
After Width: | Height: | Size: 912 B |
1
src/assets/icons/CBrushTop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749782068136" class="icon" viewBox="0 0 1137 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3340" xmlns:xlink="http://www.w3.org/1999/xlink" width="222.0703125" height="200"><path d="M853.339307 938.666667v-224.512l0.056889-0.085334C905.477973 768 938.67264 820.053333 938.67264 850.972444c0 48.355556-38.286222 87.694222-85.333333 87.694223m0-341.333334c-21.902222 18.574222-61.895111 54.613333-97.649778 98.076445-10.723556 13.027556-21.048889 26.709333-30.407111 40.760889C700.59264 773.347556 682.67264 813.226667 682.67264 850.972444a173.653333 173.653333 0 0 0 41.642667 113.066667A169.244444 169.244444 0 0 0 853.339307 1024c94.151111 0 170.666667-77.653333 170.666666-173.027556 0-99.640889-124.871111-214.812444-170.666666-253.639111m271.957333-524.515555l-711.111111 711.111111a42.439111 42.439111 0 0 1-16.696889 10.325333l-341.333333 113.777778a42.638222 42.638222 0 0 1-53.959111-53.987556l113.777777-341.333333c2.076444-6.257778 5.632-11.975111 10.296889-16.668444l483.555556-483.555556a42.723556 42.723556 0 0 1 60.359111 0 42.723556 42.723556 0 0 1 0 60.359111L344.809529 398.222222h334.392889L1064.965973 12.487111a42.723556 42.723556 0 0 1 60.359111 0 42.723556 42.723556 0 0 1 0 60.359111" p-id="3341"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/icons/CCancelGroup.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749920881941" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3277" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M797.7 442.4q-60.4 0-113.2 22.5t-91.6 62-62 92.2-23 113.2q0 50.2 17.4 97.3H133.1q-21.5 0-44.5-9.2t-42.5-26.1-31.7-41-12.3-53.8V575.6q0-47.1-0.5-100.9t-0.5-99.8V260.3q0-61.4 34.3-97.3t94.7-35.8h503.8q23.6 0 47.6 8.7t43 24.1 31.2 35.8 12.3 44v13.3H250.9q-51.2 0-67.6 57.3-8.2 29.7-18.4 63t-18.4 62q-10.2 33.8-20.5 65.5-2 8.2-2 14.3 0 16.4 11.3 28.2t28.7 11.8 26.6-12.3 14.3-27.6l56.3-196.6h541.7q18.4 3.1 35.8 11.8t30.2 23.6 18.4 36.9 0.5 51.7q0 2-0.5 5.1t-1.5 8.2q-39.9-13.3-88.1-13.3z m5.1 68.6q46.1 0 86 17.4t70.1 47.6 47.6 70.1 17.4 86-17.4 86-47.6 70.1-70.1 47.6-86 17.4-86-17.4-70.1-47.6-47.6-70.1-17.4-86 17.4-86 47.6-70.1 70.1-47.6 86-17.4z m43 218.1l67.6-68.6q9.2-9.2 9.2-22.5t-9.2-22.5-22.5-9.2-22.5 9.2l-68.6 68.6-67.6-68.6q-9.2-9.2-22.5-9.2t-22.5 9.2T678 638t9.2 22.5l67.6 68.6-67.6 67.6q-9.2 9.2-9.2 22.5t9.2 22.5q9.2 10.2 22.5 10.2t22.5-10.2l67.6-67.6 68.6 67.6q9.2 10.2 22.5 10.2t22.5-10.2q9.2-9.2 9.2-22.5t-9.2-22.5z" p-id="3278"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/CCheckbox.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749838313310" class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.390625" height="200"><path d="M533.01342 0.015975h-336.323199a187.380068 187.380068 0 0 0-138.132742 58.135868A187.380068 187.380068 0 0 0 0.421612 195.804123v336.323199a48.046171 48.046171 0 1 0 95.251534 0V210.33809a123.238429 123.238429 0 0 1 30.389204-84.0808 123.238429 123.238429 0 0 1 84.681376-30.389203h322.269694a48.046171 48.046171 0 1 0 0-95.251535z" fill="#4D4D4D" p-id="4538"></path><path d="M373.860478 538.73367l-2.402309 2.282194a46.124324 46.124324 0 0 0-1.201154 66.423831L518.959915 756.743172a46.124324 46.124324 0 0 0 65.222677 0l410.194187-410.794764a46.124324 46.124324 0 0 0 0-65.222677 46.124324 46.124324 0 0 0-65.102562 0L584.182592 621.373085a46.124324 46.124324 0 0 1-65.102562 0L437.882001 540.535402a46.24444 46.24444 0 0 0-64.021523-1.801732zM868.856157 928.868581H337.945965a72.069257 72.069257 0 0 1-72.069257-72.069257V320.483938a57.174944 57.174944 0 0 1 57.174944-57.174944h371.156673a48.046171 48.046171 0 0 0 48.046171-48.046171 48.046171 48.046171 0 0 0-48.046171-48.046172H266.35717a95.37165 95.37165 0 0 0-95.131419 95.131419v666.520511a95.37165 95.37165 0 0 0 95.131419 95.131419h665.559587a95.37165 95.37165 0 0 0 95.011303-95.131419V596.02873a48.046171 48.046171 0 0 0-47.445594-47.445594 48.046171 48.046171 0 0 0-48.046171 47.445594v269.779251a63.0606 63.0606 0 0 1-62.580138 63.0606z" fill="#4D4D4D" p-id="4539"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/assets/icons/CCheckboxList.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749838309246" class="icon" viewBox="0 0 1117 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4387" xmlns:xlink="http://www.w3.org/1999/xlink" width="218.1640625" height="200"><path d="M340.267533 68.40185m49.970214 0l677.328501 0q49.970214 0 49.970214 49.970214l0-0.136531q0 49.970214-49.970214 49.970214l-677.328501 0q-49.970214 0-49.970214-49.970214l0 0.136531q0-49.970214 49.970214-49.970214Z" fill="#4D4D4D" p-id="4388"></path><path d="M340.267533 474.853563m49.970214 0l677.328501 0q49.970214 0 49.970214 49.970214l0-0.136531q0 49.970214-49.970214 49.970214l-677.328501 0q-49.970214 0-49.970214-49.970214l0 0.136531q0-49.970214 49.970214-49.970214Z" fill="#4D4D4D" p-id="4389"></path><path d="M340.267533 881.441806m49.970214 0l677.328501 0q49.970214 0 49.970214 49.970214l0-0.136531q0 49.970214-49.970214 49.970214l-677.328501 0q-49.970214 0-49.970214-49.970214l0 0.136531q0-49.970214 49.970214-49.970214Z" fill="#4D4D4D" p-id="4390"></path><path d="M301.21977 10.64939A36.044089 36.044089 0 0 0 275.688541 0a35.907558 35.907558 0 0 0-25.53123 10.64939L123.047286 138.032476 61.745029 76.184097a36.044089 36.044089 0 0 0-25.531229-10.64939A36.180619 36.180619 0 0 0 10.68257 127.246556l87.106548 87.516139A35.771027 35.771027 0 0 0 123.047286 225.275555a35.224905 35.224905 0 0 0 25.394699-10.64939L301.21977 61.575318a36.31715 36.31715 0 0 0 0-50.925928zM301.21977 423.244981a35.907558 35.907558 0 0 0-25.531229-10.512859 35.771027 35.771027 0 0 0-25.53123 10.512859L123.047286 550.491537l-61.302257-61.84838A36.044089 36.044089 0 0 0 36.2138 477.857237a36.180619 36.180619 0 0 0-25.53123 61.848379l87.106548 87.37961a35.771027 35.771027 0 0 0 50.925928 0l152.504724-153.050847a36.453681 36.453681 0 0 0 0-50.789398zM301.21977 809.353629a36.044089 36.044089 0 0 0-25.531229-10.64939 35.907558 35.907558 0 0 0-25.53123 10.64939L123.047286 936.736715l-61.302257-61.848379a35.907558 35.907558 0 0 0-25.531229-10.51286 35.771027 35.771027 0 0 0-25.53123 10.51286 36.180619 36.180619 0 0 0 0 51.062459l87.106548 87.516139a36.044089 36.044089 0 0 0 50.925928 0L301.21977 860.143027a36.453681 36.453681 0 0 0 0-50.789398z" fill="#4D4D4D" p-id="4391"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/icons/CCloseNo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749837535549" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2589" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M622.376408 512.036566l378.313415 376.631372c30.423039 30.569304 31.08123 79.787345 1.462646 111.234236a75.545671 75.545671 0 0 1-108.747738 1.682043L512.019759 621.808156l-381.384971 379.776061a75.545671 75.545671 0 0 1-108.82087-1.462646 80.079874 80.079874 0 0 1 1.535778-111.380501L401.663111 512.036566 23.349696 135.405194A80.079874 80.079874 0 0 1 21.88705 24.24409 75.545671 75.545671 0 0 1 130.561655 22.488915L512.019759 402.264976 893.404731 22.488915a75.545671 75.545671 0 0 1 108.82087 1.462646c29.545451 31.520024 28.887261 80.738065-1.535778 111.380501L622.376408 512.036566z" p-id="2590"></path></svg>
|
||||
|
After Width: | Height: | Size: 952 B |
1
src/assets/icons/CCreateGroup.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749920877071" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27982" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M589.824 524.288q-30.72 30.72-50.688 68.096t-28.672 77.312-6.144 80.896 16.896 79.872l-388.096 0q-21.504 0-45.056-8.704t-42.496-25.6-31.744-40.96-12.8-54.784l0-438.272q0-62.464 34.304-97.792t94.72-35.328l30.72 0 60.416 0 77.824 0q44.032 0 88.576-0.512t87.552-0.512l76.8 0 56.32 0 25.6 0q23.552 0 47.616 9.216t43.008 24.576 31.232 35.84 12.288 43.008l0 12.288q-54.272 0-125.952 0.512t-144.384 0.512l-140.288 0-110.592 0q-25.6 0-40.448 15.36t-23.04 43.008q-8.192 29.696-18.432 64t-18.432 64l-20.48 69.632q-4.096 14.336 6.656 30.208t31.232 15.872q16.384 0 27.136-9.216t15.872-24.576l54.272-203.776 533.504 1.024q19.456 2.048 38.4 10.24t32.768 23.04 20.48 37.376 1.536 54.272q0 2.048-0.512 3.584t-0.512 4.608q-76.8-24.576-156.672-6.656t-140.288 78.336zM800.768 514.048q46.08 0 86.016 17.408t70.144 47.616 47.616 70.144 17.408 86.016-17.408 86.016-47.616 70.144-70.144 47.616-86.016 17.408-86.016-17.408-70.144-47.616-47.616-70.144-17.408-86.016 17.408-86.016 47.616-70.144 70.144-47.616 86.016-17.408zM929.792 763.904q13.312 0 22.528-9.216t9.216-22.528-9.216-22.528-22.528-9.216l-96.256 0 0-96.256q0-13.312-9.216-22.528t-22.528-9.216q-14.336 0-23.552 9.216t-9.216 22.528l0 96.256-95.232 0q-14.336 0-23.552 9.216t-9.216 22.528 9.216 22.528 23.552 9.216l95.232 0 0 96.256q0 13.312 9.216 22.528t23.552 9.216q13.312 0 22.528-9.216t9.216-22.528l0-96.256 96.256 0z" p-id="27983"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746501838166" class="icon" viewBox="0 0 1029 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14477" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.9765625" height="200"><path d="M133.076426 1013.469886c-1.621429-1.358835-3.271525-3.124748-4.892955-4.891808 1.79458 1.765913 3.271525 3.532972 4.892955 4.891808z m-10.939485-6.270137c-1.724632-1.574414-3.427476-3.147682-5.039732-4.892954 1.590468 1.745272 3.313953 3.31854 5.039732 4.892954zM1017.567032 158.745227c-7.794096-7.79295-19.126898-12.137783-30.123717-12.137782H776.022289V80.738903c0-44.54572-36.192036-80.738903-80.738903-80.738903H334.387028c-44.54572 0-80.738903 36.193183-80.738903 80.738903v65.868542H41.151498c-22.553227 0-41.008161 18.319624-41.008161 41.007014s18.32077 41.008161 41.008161 41.008161h36.192036v713.661492c0 44.546867 36.193183 80.738903 80.738903 80.738903h725.017227c44.546867 0 80.738903-36.192036 80.738903-80.738903V228.62262h23.584107c22.553227 0 42.149124-18.32077 42.149124-41.008161 0.022934-10.884443-4.21067-21.075135-12.003619-28.868085zM334.252865 80.986589h360.671605v66.315754h-360.670459z m548.734423 861.18744H158.083583V228.62262H882.965501v713.550262zM514.29798 366.047319c-22.799767 0-41.231767 18.432-41.231767 41.231767v382.552869c0 22.799767 18.432 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.432-41.231767-41.231767-41.231767z m-223.337496 0c-22.799767 0-41.231767 18.432-41.231767 41.231767v382.552869c0 22.799767 18.432 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.453787-41.231767-41.231767-41.231767z m444.390772 0c-22.799767 0-41.232914 18.432-41.232913 41.231767v382.552869c0 22.799767 18.433147 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.432-41.231767-41.231767-41.231767z" fill="#000000" p-id="14478"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746501838166" class="icon" viewBox="0 0 1029 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14477" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.9765625" height="200"><path d="M133.076426 1013.469886c-1.621429-1.358835-3.271525-3.124748-4.892955-4.891808 1.79458 1.765913 3.271525 3.532972 4.892955 4.891808z m-10.939485-6.270137c-1.724632-1.574414-3.427476-3.147682-5.039732-4.892954 1.590468 1.745272 3.313953 3.31854 5.039732 4.892954zM1017.567032 158.745227c-7.794096-7.79295-19.126898-12.137783-30.123717-12.137782H776.022289V80.738903c0-44.54572-36.192036-80.738903-80.738903-80.738903H334.387028c-44.54572 0-80.738903 36.193183-80.738903 80.738903v65.868542H41.151498c-22.553227 0-41.008161 18.319624-41.008161 41.007014s18.32077 41.008161 41.008161 41.008161h36.192036v713.661492c0 44.546867 36.193183 80.738903 80.738903 80.738903h725.017227c44.546867 0 80.738903-36.192036 80.738903-80.738903V228.62262h23.584107c22.553227 0 42.149124-18.32077 42.149124-41.008161 0.022934-10.884443-4.21067-21.075135-12.003619-28.868085zM334.252865 80.986589h360.671605v66.315754h-360.670459z m548.734423 861.18744H158.083583V228.62262H882.965501v713.550262zM514.29798 366.047319c-22.799767 0-41.231767 18.432-41.231767 41.231767v382.552869c0 22.799767 18.432 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.432-41.231767-41.231767-41.231767z m-223.337496 0c-22.799767 0-41.231767 18.432-41.231767 41.231767v382.552869c0 22.799767 18.432 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.453787-41.231767-41.231767-41.231767z m444.390772 0c-22.799767 0-41.232914 18.432-41.232913 41.231767v382.552869c0 22.799767 18.433147 41.231767 41.231767 41.231767s41.231767-18.432 41.231767-41.231767V407.279086c0-22.776833-18.432-41.231767-41.231767-41.231767z" p-id="14478"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
1
src/assets/icons/CDown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089438636" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4690" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M309.41 873.04625c-3.555-0.77625-7.09875-2.0025-10.6875-2.21625-3.20625-0.2025-5.74875-1.24875-8.0775-3.20624999-3.34125-2.80125-6.80624999-5.45625-9.945-8.47125001-67.6125-65.25-135.1575-130.545-202.73625-195.795-11.0025-10.59749999-17.13375-23.09625-15.71625-38.6775a36.9675 36.9675 0 0 1 7.83-19.88999999c21.81375-27.57375 55.38375-27.50625 77.0175-6.45750001 38.77875 37.77750001 77.8275 75.29625 116.76375 112.9275 0.9675 0.945 1.98 1.8675 3.7575 3.54375l0-515.07c0-25.38 15.51375-43.63875 40.5225-47.73375 26.65125-4.37625 53.58375 18.23625 53.65125 45.2475 0.10125 44.29125 0.0225 88.56 0.0225 132.84 0 163.71 0.0225 327.42 0 491.11875 0 23.6475-13.39875 42.64875-34.88625 49.4775-3.02625 0.95625-6.15375 1.58625-9.2475 2.3625l-8.26875 0z m226.45125 0c-3.43125-0.77625-6.93-1.3725-10.305-2.3625a60.22125 60.22125 0 0 1-43.30125-61.39125c1.53000001-27.855 22.43249999-51.12 50.265-55.81125001 3.36375-0.5625 6.84-0.79875 10.27125-0.79874999 119.475-0.045 238.95-0.10125 358.43625 0 29.89125 0.0225 53.6625 19.7775 59.68125 48.9825 0.315 1.54125 0.6975 3.07125 1.04625 4.60125L961.955 819.35c-0.2025 0.64125-0.50625 1.26-0.59625 1.9125a59.90625 59.90625 0 0 1-44.42625 49.8375c-2.86875 0.73125-5.77125 1.305-8.6625 1.9575l-372.40875-0.01125zM961.955 518.53625c-0.55125 2.565-1.0575 5.11875001-1.67625 7.65-6.78375 27.495-30.5325 45.97875-59.36625 46.0125-62.9775 0.05625-125.94375 0.01125-188.91 0.01125-56.32875 0-112.64625 0.05625001-168.96375-0.0225-28.65375001-0.03375-52.2675-18.4725-59.11875-45.87750001-9.29249999-37.11375 18.79875-74.0025 57.06-74.41874998 30.72375-0.32625 61.48125-0.09 92.205-0.09000001 89.13375 0 178.25625-0.0225 267.39 1e-8C929.65625 451.82375 953.48375 470.22875001 960.29 497.825c0.63 2.5425 1.13625001 5.1075 1.67625 7.65l0 13.06125-0.01125 0z m0-300.79125c-0.63 2.75625-1.1925 5.52375-1.89 8.28a59.9625 59.9625 0 0 1-57.16125 45.2925c-6.5475 0.13500001-13.095 0.05625001-19.63125 0.05625-112.8825 0-225.77625001 0-338.65875-0.03375001-24.89625 0-43.9425-10.51875-55.63125-32.61374999-19.36125-36.675 3.88125-81.3375 44.955-87.17625 0.6525-0.09000001 1.2825-0.3825 1.92375-0.6075L908.27 150.9425c0.64125 0.225 1.27125 0.51749999 1.935 0.6075a59.8275 59.8275 0 0 1 49.7925 44.46c0.73125 2.86875 1.305 5.76 1.9575 8.64l0 13.095z" p-id="4691"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746587925372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14649" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M969.806769 938.771692H370.477949l232.474256-298.614154 272.541539-354.829128a65.641026 65.641026 0 0 0-8.349539-91.871179L639.264821 13.548308a64.459487 64.459487 0 0 0-90.584616 14.25723L273.670564 381.768205 23.236923 701.80759a101.848615 101.848615 0 0 0 13.758359 142.572307l174.867692 137.58359a93.630359 93.630359 0 0 0 54.272 19.718564h703.671795c17.302974 0 31.323897-14.073436 31.323898-31.455179 0-17.381744-14.020923-31.455179-31.323898-31.45518zM73.307897 798.693744a42.010256 42.010256 0 0 1-4.174769-61.650052l213.700923-274.72082 239.143385 187.890872-213.674667 274.72082c-4.673641 5.77641-10.502564 10.502564-17.119179 13.837128h-21.267693a31.166359 31.166359 0 0 0-10.870153 2.100513 36.653949 36.653949 0 0 1-9.609847-5.041231L73.307897 798.72z" fill="#333333" p-id="14650"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746587925372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14649" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M969.806769 938.771692H370.477949l232.474256-298.614154 272.541539-354.829128a65.641026 65.641026 0 0 0-8.349539-91.871179L639.264821 13.548308a64.459487 64.459487 0 0 0-90.584616 14.25723L273.670564 381.768205 23.236923 701.80759a101.848615 101.848615 0 0 0 13.758359 142.572307l174.867692 137.58359a93.630359 93.630359 0 0 0 54.272 19.718564h703.671795c17.302974 0 31.323897-14.073436 31.323898-31.455179 0-17.381744-14.020923-31.455179-31.323898-31.45518zM73.307897 798.693744a42.010256 42.010256 0 0 1-4.174769-61.650052l213.700923-274.72082 239.143385 187.890872-213.674667 274.72082c-4.673641 5.77641-10.502564 10.502564-17.119179 13.837128h-21.267693a31.166359 31.166359 0 0 0-10.870153 2.100513 36.653949 36.653949 0 0 1-9.609847-5.041231L73.307897 798.72z" p-id="14650"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746611285722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29545" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M32 0 972.651987 0 972.651987 316.651576 931.05337 316.651576C931.05337 316.651576 851.664627 123.301587 757.99696 106.426488 664.326945 89.551388 599.217138 101.163665 599.217138 101.163665L598.048275 908.460659C598.048275 908.460659 624.046382 963.027206 666.957181 966.897965L761.948781 966.897965 761.948781 1022.63337 242.777214 1022.63337 244.09057 965.511776 327.28898 964.19607C327.28898 964.19607 389.765027 944.689555 389.765027 899.258944 389.765027 853.896474 391.083083 107.668185 391.083083 107.668185 391.083083 107.668185 288.327649 93.496156 234.939369 106.496972 181.625097 119.427304 87.881073 197.297106 73.598618 311.462763L32 312.778468 32 0 32 0 32 0Z" fill="#272636" p-id="29546"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746611285722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29545" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M32 0 972.651987 0 972.651987 316.651576 931.05337 316.651576C931.05337 316.651576 851.664627 123.301587 757.99696 106.426488 664.326945 89.551388 599.217138 101.163665 599.217138 101.163665L598.048275 908.460659C598.048275 908.460659 624.046382 963.027206 666.957181 966.897965L761.948781 966.897965 761.948781 1022.63337 242.777214 1022.63337 244.09057 965.511776 327.28898 964.19607C327.28898 964.19607 389.765027 944.689555 389.765027 899.258944 389.765027 853.896474 391.083083 107.668185 391.083083 107.668185 391.083083 107.668185 288.327649 93.496156 234.939369 106.496972 181.625097 119.427304 87.881073 197.297106 73.598618 311.462763L32 312.778468 32 0 32 0 32 0Z" p-id="29546"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
1
src/assets/icons/CGroup.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750090989124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3199" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M855.04 385.024q19.456 2.048 38.912 10.24t33.792 23.04 21.504 37.376 2.048 54.272q-2.048 8.192-8.192 40.448t-14.336 74.24-18.432 86.528-19.456 76.288q-5.12 18.432-14.848 37.888t-25.088 35.328-36.864 26.112-51.2 10.24l-567.296 0q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76l0-439.296q0-62.464 33.792-97.792t95.232-35.328l503.808 0q22.528 0 46.592 8.704t43.52 24.064 31.744 35.84 12.288 44.032l0 11.264-53.248 0q-40.96 0-95.744-0.512t-116.736-0.512-115.712-0.512-92.672-0.512l-47.104 0q-26.624 0-41.472 16.896t-23.04 44.544q-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 13.312 0 17.408 11.776 29.184t29.184 11.776q31.744 0 43.008-39.936l54.272-198.656q133.12 1.024 243.712 1.024l286.72 0z" p-id="3200"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746588005687" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16633" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M138.666667 765.781333H85.333333v84.608C85.333333 899.157333 121.173333 938.666667 165.333333 938.666667h57.770667v-58.88H165.333333c-14.72 0-26.666667-13.141333-26.666666-29.397334v-84.608z m0-169.173333H85.333333v-169.216h53.333334v169.216z m0-338.389333H85.333333V173.610667C85.333333 124.842667 121.173333 85.333333 165.333333 85.333333h57.770667v58.88H165.333333c-14.72 0-26.666667 13.141333-26.666666 29.397334v84.608z m200.021333-114.048V85.333333h115.541333v58.88H338.645333z m231.082667 0V85.333333h115.584v58.88h-115.584z m231.125333 0V85.333333h57.770667C902.826667 85.333333 938.666667 124.842667 938.666667 173.610667v84.608h-53.333334V173.610667c0-16.213333-11.946667-29.44-26.666666-29.44h-57.770667z m84.437333 283.221333H938.666667V554.666667h-53.333334v-127.274667zM454.229333 879.829333V938.666667H338.645333v-58.88h115.584z" fill="#1E2226" p-id="16634"></path><path d="M597.333333 725.333333h341.333334v85.333334h-341.333334z" fill="#1E2226" p-id="16635"></path><path d="M725.333333 938.666667v-341.333334h85.333334v341.333334z" fill="#1E2226" p-id="16636"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746588005687" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16633" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M138.666667 765.781333H85.333333v84.608C85.333333 899.157333 121.173333 938.666667 165.333333 938.666667h57.770667v-58.88H165.333333c-14.72 0-26.666667-13.141333-26.666666-29.397334v-84.608z m0-169.173333H85.333333v-169.216h53.333334v169.216z m0-338.389333H85.333333V173.610667C85.333333 124.842667 121.173333 85.333333 165.333333 85.333333h57.770667v58.88H165.333333c-14.72 0-26.666667 13.141333-26.666666 29.397334v84.608z m200.021333-114.048V85.333333h115.541333v58.88H338.645333z m231.082667 0V85.333333h115.584v58.88h-115.584z m231.125333 0V85.333333h57.770667C902.826667 85.333333 938.666667 124.842667 938.666667 173.610667v84.608h-53.333334V173.610667c0-16.213333-11.946667-29.44-26.666666-29.44h-57.770667z m84.437333 283.221333H938.666667V554.666667h-53.333334v-127.274667zM454.229333 879.829333V938.666667H338.645333v-58.88h115.584z" fill="#1E2226" p-id="16634"></path><path d="M597.333333 725.333333h341.333334v85.333334h-341.333334z" fill="#1E2226" p-id="16635"></path><path d="M725.333333 938.666667v-341.333334h85.333334v341.333334z" p-id="16636"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746500420404" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8000" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M938.633323 570.21589 554.672082 742.800573c-36.761735 15.990183-51.399706 15.990183-85.329837 0L85.366677 570.21589c-20.383048-10.82658-25.879451-50.886971-25.879451-72.745814 25.630777 14.637372 61.674122 30.959106 60.334559 30.131251l392.178726 180.787506 392.19203-180.787506c0.662106-0.165776 35.628888-17.786085 60.320232-30.131251C964.512773 519.839549 957.57959 561.377594 938.633323 570.21589zM938.633323 389.413034 554.672082 562.012044c-36.761735 15.991206-51.399706 15.991206-85.329837 0L85.366677 389.413034c-30.160116-16.004509-29.221706-66.407456 0-85.229127L469.342245 101.453646c33.930131-16.943904 55.155394-17.882276 85.329837 0l383.961241 202.730261C967.854005 320.188416 966.887964 376.225687 938.633323 389.413034zM511.999488 135.865387 119.820762 346.798471l392.178726 165.252695L904.192541 346.798471 511.999488 135.865387zM511.999488 135.865387M511.999488 889.202944l392.19203-180.815135c0.662106-0.165776 35.628888-17.786085 60.320232-30.130228 0 22.369473-6.93216 63.907519-25.879451 72.745814L554.672082 923.615709c-36.761735 15.990183-51.399706 15.990183-85.329837 0L85.366677 751.002372c-20.383048-10.82658-25.879451-50.872644-25.879451-72.745814 25.630777 14.637372 61.674122 30.959106 60.334559 30.130228L511.999488 889.202944z" fill="#272636" p-id="8001"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749783744939" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12334" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M510.3 754.4c-22.9 0-45.7-7.5-63.6-22.6L91.5 537.2l-1.9-1.7C55.8 505.1 51.7 456 80 421.1l34.7 28.2c-12.7 15.7-11.3 36.9 3.1 51.3L472.6 695l1.9 1.7c19.4 17.4 52.1 17.5 71.5 0l1.9-1.7 354.8-194.3c14.4-14.4 15.8-35.6 3.1-51.3l34.7-28.2c28.3 34.8 24.2 84-9.6 114.3l-1.9 1.7-355.2 194.6c-17.8 15-40.7 22.6-63.5 22.6z" p-id="12335"></path><path d="M510.3 946.6c-22.9 0-45.7-7.5-63.6-22.6L91.5 729.4l-1.9-1.7c-33.7-30.4-37.8-79.5-9.6-114.3l34.7 28.2c-12.7 15.7-11.3 36.9 3.1 51.3l354.8 194.3 1.9 1.7c19.4 17.4 52.1 17.5 71.5 0l1.9-1.7 354.8-194.3c14.4-14.4 15.8-35.6 3.1-51.3l34.7-28.2c28.3 34.8 24.2 84-9.6 114.3l-1.9 1.7L573.8 924c-17.8 15.1-40.7 22.6-63.5 22.6zM916.1 275.6L561 85.6c-28-25.2-73.4-25.2-101.4 0l-355.1 190c-25.3 22.8-27.7 58.4-7.2 83.6 2.2 2.7 4.5 5.2 7.2 7.7l56.1 30.7 299 163.8c28 25.2 73.4 25.2 101.4 0l299-163.8 56.1-30.7c2.7-2.4 5-5 7.2-7.7 20.4-25.2 18.1-60.8-7.2-83.6z" p-id="12336"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746587793915" class="icon" viewBox="0 0 1049 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7023" xmlns:xlink="http://www.w3.org/1999/xlink" width="204.8828125" height="200"><path d="M199.8848 263.8848a76.8 76.8 0 0 1 76.8-76.8h496.2304a76.8 76.8 0 0 1 76.8 76.8v496.2304a76.8 76.8 0 0 1-76.8 76.8H276.6848a76.8 76.8 0 0 1-76.8-76.8V263.8848z m466.816-12.8v230.528h119.04v-217.728a12.8 12.8 0 0 0-12.8-12.8h-106.24z m-64 214.4256V251.0848h-149.12l-28.7488 101.1712c-2.4064 8.4224-4.4544 16.9216-6.144 25.4976 18.1504 2.6112 36.0448 8.3712 52.8896 17.408l131.1232 70.3488z m-190.2848-23.9616c0 14.848 1.024 29.7472 3.072 44.544l39.6032 286.8224h147.584v-234.7776l-161.3824-86.6048a95.872 95.872 0 0 0-28.8768-9.984z m-59.4688-59.5456c2.4576-15.9232 5.888-31.6928 10.3168-47.232l23.7824-83.712h-110.3616a12.8 12.8 0 0 0-12.8 12.8v177.3568l20.8896-20.1984a159.6928 159.6928 0 0 1 68.1728-39.0144z m-89.088 148.1728v229.9392a12.8 12.8 0 0 0 12.8 12.8h113.8176l-38.4-278.0416c-1.9456-14.0544-3.0976-28.16-3.5072-42.24-6.912 3.8912-13.4144 8.704-19.328 14.4384l-65.3568 63.104z m402.8416 15.4368v227.328h106.24a12.8 12.8 0 0 0 12.8-12.8v-214.528h-119.04z" fill="#555555" p-id="7024"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746587793915" class="icon" viewBox="0 0 1049 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7023" xmlns:xlink="http://www.w3.org/1999/xlink" width="204.8828125" height="200"><path d="M199.8848 263.8848a76.8 76.8 0 0 1 76.8-76.8h496.2304a76.8 76.8 0 0 1 76.8 76.8v496.2304a76.8 76.8 0 0 1-76.8 76.8H276.6848a76.8 76.8 0 0 1-76.8-76.8V263.8848z m466.816-12.8v230.528h119.04v-217.728a12.8 12.8 0 0 0-12.8-12.8h-106.24z m-64 214.4256V251.0848h-149.12l-28.7488 101.1712c-2.4064 8.4224-4.4544 16.9216-6.144 25.4976 18.1504 2.6112 36.0448 8.3712 52.8896 17.408l131.1232 70.3488z m-190.2848-23.9616c0 14.848 1.024 29.7472 3.072 44.544l39.6032 286.8224h147.584v-234.7776l-161.3824-86.6048a95.872 95.872 0 0 0-28.8768-9.984z m-59.4688-59.5456c2.4576-15.9232 5.888-31.6928 10.3168-47.232l23.7824-83.712h-110.3616a12.8 12.8 0 0 0-12.8 12.8v177.3568l20.8896-20.1984a159.6928 159.6928 0 0 1 68.1728-39.0144z m-89.088 148.1728v229.9392a12.8 12.8 0 0 0 12.8 12.8h113.8176l-38.4-278.0416c-1.9456-14.0544-3.0976-28.16-3.5072-42.24-6.912 3.8912-13.4144 8.704-19.328 14.4384l-65.3568 63.104z m402.8416 15.4368v227.328h106.24a12.8 12.8 0 0 0 12.8-12.8v-214.528h-119.04z" p-id="7024"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/CMergeGroup.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750085976588" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3390" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M976.457143 365.714286c46.884571 0 49.408 23.844571 46.884571 53.028571l-42.422857 479.195429c-2.56 29.147429-8.228571 52.918857-56.32 52.918857H99.401143c-47.030857 0-53.76-23.771429-56.32-52.918857L0.694857 418.742857C-1.828571 389.558857 0.512 365.714286 47.652571 365.714286zM658.285714 768H182.857143a36.571429 36.571429 0 0 0 0 73.142857h475.428571a36.571429 36.571429 0 0 0 0-73.142857z m-182.857143-146.285714H182.857143a36.571429 36.571429 0 0 0 0 73.142857h292.571428a36.571429 36.571429 0 0 0 0-73.142857z" p-id="3391"></path><path d="M940.361143 244.041143C934.656 217.234286 906.459429 195.291429 877.714286 195.291429h-353.28c-28.818286-0.182857-69.229714-19.529143-89.307429-43.227429l-30.866286-35.84C384 92.525714 343.844571 73.142857 314.989714 73.142857H146.212571c-29.366857 0.694857-53.942857 26.368-57.929142 60.598857L73.142857 292.571429h877.714286l-10.496-48.530286z" fill-opacity=".85" p-id="3392"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746611649007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="45073" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 495.424V128a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v768a32 32 0 0 0 32 32h433.68a30.32 30.32 0 1 0 0-60.64H127.824V157.968h768.688v337.44a31.744 31.744 0 1 0 63.488 0zM630.512 928c-5.968 0-11.696-2.448-15.92-6.8A23.568 23.568 0 0 1 608 904.8V583.216c0-6.16 2.368-12.064 6.592-16.416s9.952-6.8 15.936-6.8h311.84c5.984 0 11.712 2.448 15.936 6.8s6.592 10.24 6.592 16.416v321.376c0 6.16-2.368 12.064-6.592 16.432a22.176 22.176 0 0 1-15.936 6.784L630.528 928zM672 624v240h224V624H672z m-358.096-150.432l93.392-0.24L184.16 250.176l40.848-40.848 223.136 223.136 0.24-93.392 57.68-0.16-0.48 162.912a28.96 28.96 0 0 1-28.928 28.928l-162.928 0.48 0.16-57.664z" fill="#2C2C2C" p-id="45074"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746611649007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="45073" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 495.424V128a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v768a32 32 0 0 0 32 32h433.68a30.32 30.32 0 1 0 0-60.64H127.824V157.968h768.688v337.44a31.744 31.744 0 1 0 63.488 0zM630.512 928c-5.968 0-11.696-2.448-15.92-6.8A23.568 23.568 0 0 1 608 904.8V583.216c0-6.16 2.368-12.064 6.592-16.416s9.952-6.8 15.936-6.8h311.84c5.984 0 11.712 2.448 15.936 6.8s6.592 10.24 6.592 16.416v321.376c0 6.16-2.368 12.064-6.592 16.432a22.176 22.176 0 0 1-15.936 6.784L630.528 928zM672 624v240h224V624H672z m-358.096-150.432l93.392-0.24L184.16 250.176l40.848-40.848 223.136 223.136 0.24-93.392 57.68-0.16-0.48 162.912a28.96 28.96 0 0 1-28.928 28.928l-162.928 0.48 0.16-57.664z" p-id="45074"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1016 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746503112377" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17415" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M938.666667 553.92V768c0 64.8-52.533333 117.333333-117.333334 117.333333H202.666667c-64.8 0-117.333333-52.533333-117.333334-117.333333V256c0-64.8 52.533333-117.333333 117.333334-117.333333h618.666666c64.8 0 117.333333 52.533333 117.333334 117.333333v297.92z m-64-74.624V256a53.333333 53.333333 0 0 0-53.333334-53.333333H202.666667a53.333333 53.333333 0 0 0-53.333334 53.333333v344.48A290.090667 290.090667 0 0 1 192 597.333333a286.88 286.88 0 0 1 183.296 65.845334C427.029333 528.384 556.906667 437.333333 704 437.333333c65.706667 0 126.997333 16.778667 170.666667 41.962667z m0 82.24c-5.333333-8.32-21.130667-21.653333-43.648-32.917333C796.768 511.488 753.045333 501.333333 704 501.333333c-121.770667 0-229.130667 76.266667-270.432 188.693334-2.730667 7.445333-7.402667 20.32-13.994667 38.581333-7.68 21.301333-34.453333 28.106667-51.370666 13.056-16.437333-14.634667-28.554667-25.066667-36.138667-31.146667A222.890667 222.890667 0 0 0 192 661.333333c-14.464 0-28.725333 1.365333-42.666667 4.053334V768a53.333333 53.333333 0 0 0 53.333334 53.333333h618.666666a53.333333 53.333333 0 0 0 53.333334-53.333333V561.525333zM320 480a96 96 0 1 1 0-192 96 96 0 0 1 0 192z m0-64a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" fill="#000000" p-id="17416"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750088041003" class="icon" viewBox="0 0 1243 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17251" xmlns:xlink="http://www.w3.org/1999/xlink" width="242.7734375" height="200"><path d="M962.194286 728.502857l-91.428572-128a36.571429 36.571429 0 0 0-59.245714 0l-61.44 85.942857-134.948571-192.731428a36.571429 36.571429 0 0 0-59.977143 0l-164.571429 235.154285a36.571429 36.571429 0 0 0-2.56 36.571429 36.571429 36.571429 0 0 0 32.548572 19.748571h512a36.571429 36.571429 0 0 0 29.622857-57.782857z" fill="#FFC824" p-id="17252"></path><path d="M962.194286 728.502857l-182.857143-256a36.571429 36.571429 0 0 0-59.245714 0L658.285714 556.982857l-135.68-193.828571a36.571429 36.571429 0 0 0-59.977143 0l-256 365.714285a36.571429 36.571429 0 0 0 59.977143 41.691429l226.011429-322.925714 226.011428 322.925714a36.571429 36.571429 0 0 0 29.988572 15.725714 36.571429 36.571429 0 0 0 29.988571-57.417143l-75.702857-109.714285 45.714286-64 153.234286 214.308571a36.571429 36.571429 0 0 0 29.622857 15.36 36.571429 36.571429 0 0 0 21.211428-6.948571 36.571429 36.571429 0 0 0 9.508572-49.371429zM374.857143 182.857143h-36.571429a36.571429 36.571429 0 0 1 0-73.142857h36.571429a36.571429 36.571429 0 0 1 0 73.142857z" fill="#6B400D" p-id="17253"></path><path d="M1133.714286 1024H109.714286a109.714286 109.714286 0 0 1-109.714286-109.714286V219.428571a109.714286 109.714286 0 0 1 109.714286-109.714285h82.285714a36.571429 36.571429 0 0 1 0 73.142857H109.714286a36.571429 36.571429 0 0 0-36.571429 36.571428v694.857143a36.571429 36.571429 0 0 0 36.571429 36.571429h1024a36.571429 36.571429 0 0 0 36.571428-36.571429V219.428571a36.571429 36.571429 0 0 0-36.571428-36.571428H557.714286a36.571429 36.571429 0 0 1 0-73.142857H1133.714286a109.714286 109.714286 0 0 1 109.714285 109.714285v694.857143a109.714286 109.714286 0 0 1-109.714285 109.714286z" fill="#6B400D" p-id="17254"></path><path d="M914.285714 182.857143m-146.285714 0a146.285714 146.285714 0 1 0 292.571429 0 146.285714 146.285714 0 1 0-292.571429 0Z" fill="#FFC824" p-id="17255"></path><path d="M914.285714 0a182.857143 182.857143 0 1 0 182.857143 182.857143 182.857143 182.857143 0 0 0-182.857143-182.857143z m0 292.571429a109.714286 109.714286 0 1 1 109.714286-109.714286 109.714286 109.714286 0 0 1-109.714286 109.714286z" fill="#6B400D" p-id="17256"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.4 KiB |
1
src/assets/icons/CPlus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749782138532" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7139" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M426.666667 426.666667H85.546667A85.418667 85.418667 0 0 0 0 512c0 47.445333 38.314667 85.333333 85.546667 85.333333H426.666667v341.12c0 47.274667 38.186667 85.546667 85.333333 85.546667 47.445333 0 85.333333-38.314667 85.333333-85.546667V597.333333h341.12A85.418667 85.418667 0 0 0 1024 512c0-47.445333-38.314667-85.333333-85.546667-85.333333H597.333333V85.546667A85.418667 85.418667 0 0 0 512 0c-47.445333 0-85.333333 38.314667-85.333333 85.546667V426.666667z" p-id="7140"></path></svg>
|
||||
|
After Width: | Height: | Size: 822 B |
1
src/assets/icons/CRight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749993035876" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4161" id="mx_n_1749993035877" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M247.765333 991.616c11.946667 0 24.106667-4.010667 34.218667-11.989333l529.749333-426.112a54.656 54.656 0 0 0 0-85.12L284.245333 44.501333A54.613333 54.613333 0 0 0 215.893333 129.621333l474.453334 381.397334-476.842667 383.488a54.613333 54.613333 0 0 0 34.261333 97.109333z" p-id="4162"></path></svg>
|
||||
|
After Width: | Height: | Size: 659 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747121290756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51598" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M829.888 739.392L547.84 457.28 706.112 364.8l-512-170.624L364.8 706.112l92.48-158.336 282.112 282.112z" fill="#040000" p-id="51599"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747121290756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51598" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M829.888 739.392L547.84 457.28 706.112 364.8l-512-170.624L364.8 706.112l92.48-158.336 282.112 282.112z" p-id="51599"></path></svg>
|
||||
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 465 B |
1
src/assets/icons/CSort.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749838755945" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5511" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M346.741022 0h126.756232v126.756232H346.741022zM550.502746 0h126.756232v126.756232H550.502746zM346.741022 224.52725h126.756232v126.756231H346.741022zM550.502746 224.52725h126.756232v126.756231H550.502746zM346.741022 448.621884h126.756232v126.756232H346.741022zM550.502746 448.621884h126.756232v126.756232H550.502746zM346.741022 673.149134h126.756232v126.756231H346.741022zM550.502746 673.149134h126.756232v126.756231H550.502746zM346.741022 897.243768h126.756232v126.756232H346.741022zM550.502746 897.243768h126.756232v126.756232H550.502746z" p-id="5512"></path></svg>
|
||||
|
After Width: | Height: | Size: 900 B |
1
src/assets/icons/CTop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089460412" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4842" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.095 911c23.749 0 43-19.252 43-43V449.812l104.69 104.689c16.793 16.793 44.019 16.793 60.812 0 16.792-16.792 16.792-44.019 0-60.811L544.92 318.013c-0.735-0.86-1.51-1.7-2.325-2.514-8.42-8.42-19.464-12.619-30.5-12.594-11.037-0.025-22.081 4.174-30.502 12.594a43.591 43.591 0 0 0-2.324 2.514L303.594 493.69c-16.792 16.792-16.792 44.019 0 60.811 16.793 16.793 44.019 16.793 60.812 0l104.689-104.689V868c0 23.748 19.252 43 43 43zM820 200c23.748 0 43-19.252 43-43s-19.252-43-43-43H204c-23.748 0-43 19.252-43 43s19.252 43 43 43h616z" p-id="4843"></path></svg>
|
||||
|
After Width: | Height: | Size: 888 B |
1
src/assets/icons/CUp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089434755" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4538" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M714.59 150.95375c3.555 0.77625 7.09875 2.0025 10.6875 2.21625 3.20625 0.2025 5.74875 1.24875 8.0775 3.20624999 3.34125 2.80125 6.80624999 5.45625 9.945 8.47125001 67.6125 65.25 135.1575 130.545 202.73625 195.795 11.0025 10.59749999 17.13375 23.09625 15.71625 38.6775a36.9675 36.9675 0 0 1-7.83 19.88999999c-21.81375 27.57375-55.38375 27.50625-77.0175 6.45750001-38.77875-37.77750001-77.8275-75.29625-116.76375-112.9275-0.9675-0.945-1.98-1.8675-3.7575-3.54375l0 515.07c0 25.38-15.51375 43.63875-40.5225 47.73375-26.65125 4.37625-53.58375-18.23625-53.65125-45.2475-0.10125-44.29125-0.0225-88.56-0.0225-132.84 0-163.71-0.0225-327.42 0-491.11875 0-23.6475 13.39875-42.64875 34.88625-49.4775 3.02625-0.95625 6.15375-1.58625 9.2475-2.3625l8.26875 0z m-226.45125 0c3.43125 0.77625 6.93 1.3725 10.305 2.3625a60.22125 60.22125 0 0 1 43.30125 61.39125c-1.53000001 27.855-22.43249999 51.12-50.265 55.81125001-3.36375 0.5625-6.84 0.79875-10.27125 0.79874999-119.475 0.045-238.95 0.10125-358.43625 0-29.89125-0.0225-53.6625-19.7775-59.68125-48.9825-0.315-1.54125-0.6975-3.07125-1.04625-4.60125L62.045 204.65c0.2025-0.64125 0.50625-1.26 0.59625-1.9125a59.90625 59.90625 0 0 1 44.42625-49.8375c2.86875-0.73125 5.77125-1.305 8.6625-1.9575l372.40875 0.01125zM62.045 505.46375c0.55125-2.565 1.0575-5.11875001 1.67625-7.65 6.78375-27.495 30.5325-45.97875 59.36625-46.0125 62.9775-0.05625 125.94375-0.01125 188.91-0.01125 56.32875 0 112.64625-0.05625001 168.96375 0.0225 28.65375001 0.03375 52.2675 18.4725 59.11875 45.87750001 9.29249999 37.11375-18.79875 74.0025-57.06 74.41874998-30.72375 0.32625-61.48125 0.09-92.205 0.09000001-89.13375 0-178.25625 0.0225-267.39-1e-8C94.34375 572.17625 70.51625 553.77124999 63.71 526.175c-0.63-2.5425-1.13625001-5.1075-1.67625-7.65l0-13.06125 0.01125 0z m0 300.79125c0.63-2.75625 1.1925-5.52375 1.89-8.28a59.9625 59.9625 0 0 1 57.16125-45.2925c6.5475-0.13500001 13.095-0.05625001 19.63125-0.05625 112.8825 0 225.77625001 0 338.65875 0.03375001 24.89625 0 43.9425 10.51875 55.63125 32.61374999 19.36125 36.675-3.88125 81.3375-44.955 87.17625-0.6525 0.09000001-1.2825 0.3825-1.92375 0.6075L115.73 873.0575c-0.64125-0.225-1.27125-0.51749999-1.935-0.6075a59.8275 59.8275 0 0 1-49.7925-44.46c-0.73125-2.86875-1.305-5.76-1.9575-8.64l0-13.095z" fill="" p-id="4539"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
src/assets/icons/fonticon/CFcenter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749746246836" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4311" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M929.664 251.093333a52.096 52.096 0 0 0 0-104.192H94.976a52.096 52.096 0 0 0 0 104.192z m-208.682667 208.682667a52.138667 52.138667 0 0 0 0-104.234667H303.658667a52.138667 52.138667 0 0 0 0 104.234667zM981.546667 616.192a52.181333 52.181333 0 0 1-52.096 52.096H94.976a52.096 52.096 0 1 1 0-104.192h834.688A52.181333 52.181333 0 0 1 981.546667 616.192m-260.778667 260.992a52.138667 52.138667 0 0 0 0-104.234667H303.658667a52.138667 52.138667 0 0 0 0 104.234667z" p-id="4312"></path></svg>
|
||||
|
After Width: | Height: | Size: 820 B |
1
src/assets/icons/fonticon/CFleft.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749746098847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2369" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M929.28 250.538667a52.096 52.096 0 0 0 0-104.192H94.634667a52.096 52.096 0 0 0 0 104.192zM511.872 459.221333a52.138667 52.138667 0 0 0 0-104.234666H94.634667a52.138667 52.138667 0 0 0 0 104.234666zM981.205333 615.466667a52.181333 52.181333 0 0 1-52.096 52.096H94.634667a52.096 52.096 0 1 1 0-104.192h834.645333A52.181333 52.181333 0 0 1 981.205333 615.466667m-469.333333 261.077333a52.138667 52.138667 0 0 0 0-104.234667H94.634667a52.138667 52.138667 0 0 0 0 104.234667z" p-id="2370"></path></svg>
|
||||
|
After Width: | Height: | Size: 830 B |
1
src/assets/icons/fonticon/CFright.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749746222897" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3327" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M929.578667 251.093333a52.096 52.096 0 0 0 0-104.192H94.933333a52.096 52.096 0 1 0 0 104.192z m0 208.682667a52.138667 52.138667 0 0 0 0-104.234667H512.128a52.138667 52.138667 0 0 0 0 104.234667zM981.461333 616.192a52.181333 52.181333 0 0 1-52.096 52.096H94.933333a52.096 52.096 0 1 1 0-104.192h834.645334A52.181333 52.181333 0 0 1 981.461333 616.192m-52.096 260.992a52.138667 52.138667 0 0 0 0-104.234667H512.128a52.138667 52.138667 0 0 0 0 104.234667z" p-id="3328"></path></svg>
|
||||
|
After Width: | Height: | Size: 812 B |
1
src/assets/icons/fonticon/CFtretch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749746899231" class="icon" viewBox="0 0 1462 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8628" xmlns:xlink="http://www.w3.org/1999/xlink" width="285.546875" height="200"><path d="M258.19383386 353.04924526h953.4008432c14.61924623 0 27.07032366 5.2050163 37.45916917 15.5232382 10.33234661 10.40297098 15.52323819 22.84698604 15.52323819 37.47329462s-5.19089157 27.06326129-15.52323819 37.46623226c-10.38178315 10.33940898-22.83286131 15.52323819-37.45916917 15.5232382H258.19383386c-14.61924623 0-27.07032366-5.18382922-37.4591699-15.5232382C210.42350516 433.10197701 205.21142577 420.67208668 205.21142577 406.04577808c0-14.61924623 5.21207939-27.07032366 15.52323819-37.47329462 10.38178315-10.31115879 22.83286131-15.52323819 37.4591699-15.5232382m0 211.85194421h953.4008432c14.61924623 0 27.07032366 5.16264212 37.45916917 15.55148836 10.33234661 10.33234661 15.52323819 22.82579894 15.52323819 37.43091973 0 14.62630859-5.19089157 27.11976093-15.52323819 37.43091973-10.38178315 10.41003333-22.83286131 15.58680018-37.45916917 15.58680018H258.19383386c-14.61924623 0-27.07032366-5.17676685-37.4591699-15.57973783C210.42350516 645.01042084 205.21142577 632.50990616 205.21142577 617.89065992s5.21207939-27.10563621 15.52323819-37.43091972c10.38178315-10.39590861 22.83286131-15.55855001 37.4591699-15.55855074m0 211.89431911h953.4008432c14.61924623 0 27.07032366 5.13439194 37.45916917 15.55148837 10.33234661 10.31115879 15.52323819 22.79754877 15.52323819 37.43091973 0 14.63337096-5.19089157 27.11976093-15.52323819 37.43091973-10.38178315 10.39590861-22.83286131 15.55855001-37.45916917 15.55855073H258.19383386c-14.61924623 0-27.07032366-5.16264212-37.4591699-15.55148838-10.31115879-10.31115879-15.52323819-22.8116735-15.52323819-37.43091971 0-14.64043332 5.21207939-27.11976093 15.52323819-37.43091972 10.38178315-10.42415807 22.83286131-15.55855001 37.4591699-15.55855075M258.19383386 141.21142578h953.4008432c14.61924623 0 27.07032366 5.15557975 37.45916917 15.53736293C1259.38619285 167.05994749 1264.57708442 179.56752528 1264.57708442 194.17970842c0 14.64043332-5.19089157 27.13388565-15.52323819 37.43092045-10.38178315 10.42415807-22.83286131 15.57267547-37.45916917 15.57267474H258.19383386c-14.61924623 0-27.07032366-5.15557975-37.4591699-15.56561237C210.42350516 221.3135948 205.21142577 208.82014247 205.21142577 194.17970842c0-14.61924623 5.21207939-27.11976093 15.52323819-37.43091971C231.11644712 146.35288008 243.56752527 141.21142578 258.19383386 141.21142578" p-id="8629"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1911,6 +1911,7 @@ textarea:focus {
|
||||
padding-right: 0;
|
||||
background-color: #fff;
|
||||
flex: 1;
|
||||
height: 5rem;
|
||||
}
|
||||
.collection_modal_body .input_border .input_box_btnBox.sketch,
|
||||
.design_detail_modal_component .input_border .input_box_btnBox.sketch,
|
||||
@@ -1922,6 +1923,33 @@ textarea:focus {
|
||||
.generalMenu_printModel_upload .input_border .input_box_btnBox.sketch,
|
||||
.generate .input_border .input_box_btnBox.sketch {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.collection_modal_body .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.design_detail_modal_component .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.library_page .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.productImg_content .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.poseTransfer .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.scaleImage_modal .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.accountEdit_page .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.generalMenu_printModel_upload .input_border .input_box_btnBox.sketch .upload_item,
|
||||
.generate .input_border .input_box_btnBox.sketch .upload_item {
|
||||
width: 6rem;
|
||||
}
|
||||
.collection_modal_body .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.design_detail_modal_component .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.library_page .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.productImg_content .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.poseTransfer .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.scaleImage_modal .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.accountEdit_page .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.generalMenu_printModel_upload .input_border .input_box_btnBox.sketch .upload_item .upload_file_item,
|
||||
.generate .input_border .input_box_btnBox.sketch .upload_item .upload_file_item {
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
.collection_modal_body .input_border .input_box_btnBox > .textarea,
|
||||
.design_detail_modal_component .input_border .input_box_btnBox > .textarea,
|
||||
@@ -1977,7 +2005,7 @@ textarea:focus {
|
||||
.accountEdit_page .input_border .input_box_btnBox .upload_item,
|
||||
.generalMenu_printModel_upload .input_border .input_box_btnBox .upload_item,
|
||||
.generate .input_border .input_box_btnBox .upload_item {
|
||||
width: 5.7rem;
|
||||
width: 4.7rem;
|
||||
}
|
||||
.collection_modal_body .input_border .input_box_btnBox .upload_item .upload_file_item,
|
||||
.design_detail_modal_component .input_border .input_box_btnBox .upload_item .upload_file_item,
|
||||
@@ -1992,8 +2020,8 @@ textarea:focus {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 5.7rem;
|
||||
width: 5.7rem;
|
||||
height: 4.7rem;
|
||||
width: 4.7rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -2087,7 +2115,6 @@ textarea:focus {
|
||||
z-index: 4;
|
||||
width: 4rem;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
justify-content: center;
|
||||
}
|
||||
.collection_modal_body .input_border .fi.fi-br-loading,
|
||||
@@ -2214,7 +2241,7 @@ textarea:focus {
|
||||
.generage_btn_box .generage_btn {
|
||||
margin-left: 2rem;
|
||||
display: flex;
|
||||
font-size: 1.8rem;
|
||||
font-size: 1.6rem;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: content-box;
|
||||
justify-content: center;
|
||||
@@ -2250,12 +2277,13 @@ textarea:focus {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #fff;
|
||||
}
|
||||
.generage_btn_box .content > div.active {
|
||||
background-color: #616161;
|
||||
background-color: #000;
|
||||
}
|
||||
.generage_btn_box .content > div:hover {
|
||||
background: #999999;
|
||||
background: #000;
|
||||
}
|
||||
.hideChecked {
|
||||
user-select: none;
|
||||
|
||||
@@ -1986,9 +1986,21 @@ textarea:focus{
|
||||
padding-right: 0;
|
||||
background-color: #fff;
|
||||
flex: 1;
|
||||
height: 5rem;
|
||||
// border
|
||||
&.sketch{
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
.upload_item{
|
||||
width: 6rem;
|
||||
.upload_file_item{
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .textarea{
|
||||
border-radius: 1rem;
|
||||
@@ -2013,14 +2025,14 @@ textarea:focus{
|
||||
border-right: calc(0.1rem* 1.2) solid #F1F1F1;
|
||||
}
|
||||
.upload_item{
|
||||
width: 5.7rem;
|
||||
width: 4.7rem;
|
||||
.upload_file_item{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 5.7rem;
|
||||
width: 5.7rem;
|
||||
height: 4.7rem;
|
||||
width: 4.7rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -2064,7 +2076,6 @@ textarea:focus{
|
||||
z-index: 4;
|
||||
width: 4rem;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
justify-content: center;
|
||||
&.fi-br-loading{
|
||||
height: 100%;
|
||||
@@ -2150,7 +2161,7 @@ textarea:focus{
|
||||
// margin: 0 auto;
|
||||
margin-left: 2rem;
|
||||
display: flex;
|
||||
font-size: 1.8rem;
|
||||
font-size: 1.6rem;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: content-box;
|
||||
justify-content: center;
|
||||
@@ -2185,12 +2196,13 @@ textarea:focus{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #fff;
|
||||
&.active{
|
||||
background-color: #616161;
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
>div:hover{
|
||||
background: #999999;
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command } from "./Command";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 创建背景图层命令
|
||||
@@ -195,10 +195,13 @@ export class BackgroundSizeCommand extends Command {
|
||||
this.newWidth = options.newWidth;
|
||||
this.newHeight = options.newHeight;
|
||||
this.historyManager = options.historyManager;
|
||||
this.isRedGreenMode = options.isRedGreenMode;
|
||||
|
||||
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
|
||||
|
||||
// 记录原尺寸
|
||||
this.oldWidth = this.canvas.width;
|
||||
this.oldHeight = this.canvas.height;
|
||||
this.oldWidth = this.bgLayer.fabricObject.width;
|
||||
this.oldHeight = this.bgLayer.fabricObject.height;
|
||||
|
||||
// 查找背景图层
|
||||
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
|
||||
|
||||
@@ -609,12 +609,15 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
...options,
|
||||
name: `上传纹理: ${options.name || options.file?.name || '未知'}`,
|
||||
name: `上传纹理: ${options.name || options.file?.name || "未知"}`,
|
||||
description: `上传自定义纹理文件`,
|
||||
});
|
||||
|
||||
this.file = options.file;
|
||||
this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理";
|
||||
this.name =
|
||||
options.name ||
|
||||
options.file?.name?.replace(/\.[^/.]+$/, "") ||
|
||||
"自定义纹理";
|
||||
this.category = options.category || "自定义材质";
|
||||
this.texturePresetManager = options.texturePresetManager;
|
||||
this.brushManager = options.brushManager;
|
||||
@@ -623,13 +626,13 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
|
||||
async execute() {
|
||||
if (!this.file || !this.texturePresetManager) {
|
||||
throw new Error('缺少必要的文件或纹理预设管理器');
|
||||
throw new Error("缺少必要的文件或纹理预设管理器");
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建文件 data URL
|
||||
const dataUrl = await this._fileToDataUrl(this.file);
|
||||
|
||||
|
||||
// 添加到纹理预设管理器
|
||||
this.uploadedTextureId = this.texturePresetManager.addCustomTexture({
|
||||
name: this.name,
|
||||
@@ -654,10 +657,10 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
return {
|
||||
textureId: this.uploadedTextureId,
|
||||
dataUrl: dataUrl,
|
||||
name: this.name
|
||||
name: this.name,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('纹理上传失败:', error);
|
||||
console.error("纹理上传失败:", error);
|
||||
throw new Error(`纹理上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -672,7 +675,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
this.texturePresetManager.removeCustomTexture(this.uploadedTextureId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('撤销纹理上传失败:', error);
|
||||
console.error("撤销纹理上传失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -680,7 +683,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
/**
|
||||
* 将文件转换为 data URL
|
||||
* @private
|
||||
* @param {File} file
|
||||
* @param {File} file
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
_fileToDataUrl(file) {
|
||||
|
||||
100
src/component/Canvas/CanvasEditor/commands/EraserCommand.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { isArray } from "lodash-es";
|
||||
import { optimizeCanvasRendering } from "../utils/helper";
|
||||
import { restoreObjectLayerAssociations } from "../utils/layerUtils";
|
||||
import { Command } from "./Command";
|
||||
|
||||
/**
|
||||
* 橡皮擦操作命令
|
||||
* 支持橡皮擦操作的撤销和重做
|
||||
*/
|
||||
export class EraserCommand extends Command {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} options 命令选项
|
||||
* @param {Object} options.canvas Fabric画布实例
|
||||
* @param {Object} options.layerManager 图层管理器
|
||||
* @param {Object} options.beforeSnapshot 擦除前的状态快照
|
||||
* @param {Object} options.afterSnapshot 擦除后的状态快照
|
||||
* @param {Array} options.affectedObjects 擦除影响的对象列表(可选)
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "橡皮擦操作",
|
||||
description: `擦除`,
|
||||
...options,
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.beforeSnapshot = options.beforeSnapshot;
|
||||
this.afterSnapshot = options.afterSnapshot;
|
||||
this.affectedObjects = options.affectedObjects || [];
|
||||
this.fristLoad = true; // 是否是第一次加载
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行橡皮擦操作
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.beforeSnapshot || !this.afterSnapshot) {
|
||||
console.warn("缺少状态快照,无法执行橡皮擦命令");
|
||||
return false;
|
||||
}
|
||||
if (!this.fristLoad)
|
||||
await this._restoreCanvasState(this.afterSnapshot); // 应用重做的状态
|
||||
else await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
this.fristLoad = false; // 标记为非第一次加载
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销橡皮擦操作
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.beforeSnapshot) {
|
||||
console.warn("缺少擦除前的状态快照,无法撤销");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 恢复到擦除前的状态
|
||||
await this._restoreCanvasState(this.beforeSnapshot);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布状态
|
||||
* @param {Object} snapshot 状态快照
|
||||
* @private
|
||||
*/
|
||||
async _restoreCanvasState(snapshot) {
|
||||
// 对比 eraser erasable 两个属性,如果当前对象的eraser属性和erasable属性不一致,则需要更新对象的eraser属性
|
||||
if (!snapshot || !snapshot.objects) return;
|
||||
// 优化渲染 - 统一批处理 支持异步回调 防止闪屏
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
return new Promise((resolve) => {
|
||||
this.canvas.loadFromJSON(snapshot, async () => {
|
||||
// 恢复图层关联
|
||||
this._restoreObjectLayerAssociations();
|
||||
// 确保所有对象的交互性正确设置
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象与图层的关联关系
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectLayerAssociations() {
|
||||
if (!this.layerManager) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
restoreObjectLayerAssociations(
|
||||
this.layerManager?.layers?.value,
|
||||
canvasObjects
|
||||
);
|
||||
}
|
||||
}
|
||||
1087
src/component/Canvas/CanvasEditor/commands/GroupCommands.js
Normal file
@@ -3,7 +3,7 @@ import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
|
||||
import { ToolCommand } from "./ToolCommands.js";
|
||||
import { ClearSelectionCommand } from "./SelectionCommands.js";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 套索抠图命令
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Command, FunctionCommand } from "./Command";
|
||||
import { getLiquifyReferenceManager } from "../managers/LiquifyReferenceManager";
|
||||
|
||||
/**
|
||||
* 液化命令基类
|
||||
@@ -192,81 +193,6 @@ export class LiquifyCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层栅格化命令
|
||||
* 用于将复杂图层栅格化为单一图像,以便进行液化操作
|
||||
*/
|
||||
export class RasterizeForLiquifyCommand extends Command {
|
||||
/**
|
||||
* 创建栅格化命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
* @param {Object} options.layerManager 图层管理器实例
|
||||
* @param {String} options.layerId 需要栅格化的图层ID
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "栅格化图层",
|
||||
description: options.description || "将图层栅格化为单一图像以便液化操作",
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.originalLayer = null;
|
||||
this.rasterizedImageObj = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行栅格化操作
|
||||
* @returns {Promise<Object>} 栅格化后的图像对象
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.canvas || !this.layerManager || !this.layerId) {
|
||||
throw new Error("栅格化命令缺少必要参数");
|
||||
}
|
||||
|
||||
// 保存原始图层信息
|
||||
this.originalLayer = this.layerManager.getLayerById(this.layerId);
|
||||
if (!this.originalLayer) {
|
||||
throw new Error(`图层ID不存在: ${this.layerId}`);
|
||||
}
|
||||
|
||||
// 栅格化图层
|
||||
const rasterizedImage = await this.layerManager.rasterizeLayer(
|
||||
this.layerId
|
||||
);
|
||||
if (!rasterizedImage) {
|
||||
throw new Error("栅格化图层失败");
|
||||
}
|
||||
|
||||
this.rasterizedImageObj = rasterizedImage;
|
||||
return rasterizedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销栅格化操作
|
||||
* 注意:完整撤销栅格化是复杂的,这里提供近似还原
|
||||
* @returns {Promise<boolean>} 撤销结果
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.canvas || !this.layerManager || !this.originalLayer) {
|
||||
throw new Error("无法撤销:缺少必要的状态信息");
|
||||
}
|
||||
|
||||
// 恢复图层为原始状态是复杂的,这里可能需要与LayerManager协作
|
||||
// 这个实现可能需要根据实际的LayerManager功能来调整
|
||||
const restored = await this.layerManager.restoreLayerFromBackup(
|
||||
this.layerId
|
||||
);
|
||||
if (!restored) {
|
||||
console.warn("无法完全还原栅格化前的图层状态");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 液化工具初始化命令
|
||||
* 用于初始化液化工具的状态,不执行实际操作
|
||||
@@ -550,6 +476,405 @@ export class LiquifyResetCommand extends LiquifyCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 液化状态命令 - 最优化版本
|
||||
* 使用引用管理器避免对象引用丢失,支持高性能的状态管理
|
||||
*/
|
||||
export class LiquifyStateCommand extends Command {
|
||||
/**
|
||||
* 创建液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
* @param {Object} options.layerManager 图层管理器实例
|
||||
* @param {Object} options.targetObject 目标对象(保持引用)
|
||||
* @param {String} options.targetLayerId 目标图层ID
|
||||
* @param {ImageData} options.initialImageData 初始图像数据
|
||||
* @param {ImageData} options.finalImageData 最终图像数据
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "液化操作",
|
||||
description: options.description || "液化变形操作的状态记录",
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.targetObject = options.targetObject;
|
||||
this.targetLayerId = options.targetLayerId;
|
||||
this.targetObjectId = options.targetObjectId;
|
||||
|
||||
// 获取引用管理器实例
|
||||
this.refManager = getLiquifyReferenceManager();
|
||||
|
||||
// 注册对象到引用管理器
|
||||
this.objectRefId = this.refManager.registerObject(
|
||||
this.targetObject,
|
||||
this.targetObjectId || `liquify_${Date.now()}`
|
||||
);
|
||||
|
||||
// 保存状态快照ID
|
||||
this.initialSnapshotId = null;
|
||||
this.finalSnapshotId = null;
|
||||
|
||||
// 设置图像数据
|
||||
this.initialImageData = options.initialImageData;
|
||||
this.finalImageData = options.finalImageData;
|
||||
|
||||
this.currentState = "initial";
|
||||
|
||||
// 创建初始快照
|
||||
if (this.initialImageData) {
|
||||
this._createInitialSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令 - 应用最终状态
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.finalImageData) {
|
||||
throw new Error("缺少最终状态数据");
|
||||
}
|
||||
|
||||
// 确保有最终状态快照
|
||||
if (!this.finalSnapshotId) {
|
||||
await this._createFinalSnapshot();
|
||||
}
|
||||
|
||||
// 通过引用管理器更新图像数据
|
||||
await this.refManager.updateObjectImageData(
|
||||
this.objectRefId,
|
||||
this.finalImageData
|
||||
);
|
||||
|
||||
this.currentState = "final";
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("✅ 液化命令执行完成,应用最终状态");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销命令 - 恢复初始状态
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.initialImageData || !this.initialSnapshotId) {
|
||||
throw new Error("缺少初始状态数据或快照");
|
||||
}
|
||||
|
||||
// 通过引用管理器恢复到初始快照
|
||||
await this.refManager.restoreFromSnapshot(
|
||||
this.objectRefId,
|
||||
this.initialSnapshotId
|
||||
);
|
||||
|
||||
this.currentState = "initial";
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("🔄 液化命令撤销完成,恢复初始状态");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做命令 - 重新应用最终状态
|
||||
*/
|
||||
async redo() {
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最终状态的图像数据
|
||||
* @param {ImageData} finalImageData 最终状态的图像数据
|
||||
*/
|
||||
setFinalImageData(finalImageData) {
|
||||
this.finalImageData = finalImageData;
|
||||
this.finalSnapshotId = null; // 重置快照ID,下次执行时重新创建
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态标记
|
||||
* @returns {String} 'initial' 或 'final'
|
||||
*/
|
||||
getCurrentState() {
|
||||
return this.currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查命令是否有效
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
isValid() {
|
||||
const targetObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
return !!(targetObject && this.initialImageData && this.finalImageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标对象的当前引用
|
||||
* @returns {Object|null} Fabric对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.refManager.getObjectRef(this.objectRefId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
if (this.objectRefId) {
|
||||
// 清理快照
|
||||
if (this.initialSnapshotId) {
|
||||
this.refManager.stateSnapshots.delete(this.initialSnapshotId);
|
||||
}
|
||||
if (this.finalSnapshotId) {
|
||||
this.refManager.stateSnapshots.delete(this.finalSnapshotId);
|
||||
}
|
||||
|
||||
// 注意:不要清理对象引用,因为可能有其他命令在使用
|
||||
console.log(`🗑️ 液化状态命令资源已清理: ${this.objectRefId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getMemoryStats() {
|
||||
const refManagerStats = this.refManager.getMemoryStats();
|
||||
const commandMemory = this._calculateCommandMemory();
|
||||
|
||||
return {
|
||||
...refManagerStats,
|
||||
commandMemory,
|
||||
totalCommandMemory: commandMemory,
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 创建初始状态快照
|
||||
* @private
|
||||
*/
|
||||
async _createInitialSnapshot() {
|
||||
if (!this.initialImageData) return;
|
||||
|
||||
this.initialSnapshotId = `${this.objectRefId}_initial_${Date.now()}`;
|
||||
|
||||
// 手动创建初始快照,包含图像数据
|
||||
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this.refManager._captureObjectState(fabricObject),
|
||||
imageData: this.initialImageData,
|
||||
eventListeners:
|
||||
this.refManager.eventListeners.get(this.objectRefId) || {},
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.initialSnapshotId, snapshot);
|
||||
console.log(`📸 初始状态快照已创建: ${this.initialSnapshotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最终状态快照
|
||||
* @private
|
||||
*/
|
||||
async _createFinalSnapshot() {
|
||||
if (!this.finalImageData) return;
|
||||
|
||||
this.finalSnapshotId = `${this.objectRefId}_final_${Date.now()}`;
|
||||
|
||||
// 手动创建最终快照,包含图像数据
|
||||
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this.refManager._captureObjectState(fabricObject),
|
||||
imageData: this.finalImageData,
|
||||
eventListeners:
|
||||
this.refManager.eventListeners.get(this.objectRefId) || {},
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.finalSnapshotId, snapshot);
|
||||
console.log(`📸 最终状态快照已创建: ${this.finalSnapshotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算命令本身的内存使用量
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* @private
|
||||
*/
|
||||
_calculateCommandMemory() {
|
||||
let bytes = 0;
|
||||
|
||||
// 计算ImageData内存使用
|
||||
if (this.initialImageData) {
|
||||
bytes += this.initialImageData.width * this.initialImageData.height * 4;
|
||||
}
|
||||
if (this.finalImageData) {
|
||||
bytes += this.finalImageData.width * this.finalImageData.height * 4;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量液化状态命令 - 用于处理多个对象的液化操作
|
||||
*/
|
||||
export class BatchLiquifyStateCommand extends Command {
|
||||
/**
|
||||
* 创建批量液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Array} options.commands 液化状态命令列表
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "批量液化操作",
|
||||
description:
|
||||
options.description || `批量液化${options.commands?.length || 0}个对象`,
|
||||
});
|
||||
|
||||
this.commands = options.commands || [];
|
||||
this.canvas = options.canvas;
|
||||
this.refManager = getLiquifyReferenceManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加液化状态命令
|
||||
* @param {LiquifyStateCommand} command 液化状态命令
|
||||
*/
|
||||
addCommand(command) {
|
||||
this.commands.push(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量操作
|
||||
*/
|
||||
async execute() {
|
||||
const results = [];
|
||||
const updates = [];
|
||||
|
||||
// 准备批量更新数据
|
||||
for (const command of this.commands) {
|
||||
if (command.finalImageData && command.objectRefId) {
|
||||
updates.push({
|
||||
refId: command.objectRefId,
|
||||
imageData: command.finalImageData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 执行批量更新
|
||||
if (updates.length > 0) {
|
||||
const updateResults = await this.refManager.batchUpdate(updates);
|
||||
results.push(...updateResults);
|
||||
}
|
||||
|
||||
// 更新命令状态
|
||||
this.commands.forEach((command) => {
|
||||
command.currentState = "final";
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销批量操作
|
||||
*/
|
||||
async undo() {
|
||||
// 逆序撤销
|
||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
||||
await this.commands[i].undo();
|
||||
}
|
||||
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做批量操作
|
||||
*/
|
||||
async redo() {
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查批量命令是否有效
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
isValid() {
|
||||
return (
|
||||
this.commands.length > 0 && this.commands.every((cmd) => cmd.isValid())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有子命令的资源
|
||||
*/
|
||||
dispose() {
|
||||
this.commands.forEach((command) => {
|
||||
if (command.dispose) {
|
||||
command.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化fabric对象为JSON
|
||||
* @param {Object} fabricObject fabric对象
|
||||
* @returns {Object} 序列化后的对象状态
|
||||
*/
|
||||
export function serializeFabricObject(fabricObject) {
|
||||
if (!fabricObject) {
|
||||
throw new Error("无法序列化:对象为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用fabric.js的toObject方法序列化
|
||||
const serializedObject = fabricObject.toObject([
|
||||
"id",
|
||||
"objectId",
|
||||
"uid",
|
||||
"layerId",
|
||||
"name",
|
||||
"type",
|
||||
]);
|
||||
|
||||
// 记录额外的元数据
|
||||
const metadata = {
|
||||
timestamp: Date.now(),
|
||||
objectType: fabricObject.type,
|
||||
objectId: fabricObject.id || fabricObject.objectId || fabricObject.uid,
|
||||
layerId: fabricObject.layerId,
|
||||
bounds: fabricObject.getBoundingRect(),
|
||||
};
|
||||
|
||||
return {
|
||||
serializedObject,
|
||||
metadata,
|
||||
version: "1.0",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("序列化fabric对象失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:创建液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @returns {LiquifyStateCommand} 状态命令实例
|
||||
*/
|
||||
export function createLiquifyStateCommand(options) {
|
||||
return new LiquifyStateCommand(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:创建液化重置命令
|
||||
* @param {Object} options 配置选项
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { OperationType } from "../utils/layerHelper.js";
|
||||
import {
|
||||
findObjectById,
|
||||
generateId,
|
||||
optimizeCanvasRendering,
|
||||
} from "../utils/helper.js";
|
||||
import { LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
import { Command, CompositeCommand } from "./Command.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 批量初始化红绿图模式命令
|
||||
@@ -29,100 +34,134 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
this.originalNormalObjects = null;
|
||||
this.originalNormalOpacities = new Map();
|
||||
this.originalToolState = null;
|
||||
this.originalActiveLayerId = null;
|
||||
|
||||
// 存储加载的图片对象
|
||||
this.clothingImage = null;
|
||||
this.redGreenImage = null;
|
||||
|
||||
// 存储新创建的图层ID
|
||||
this.newEmptyLayerId = null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
// 禁用画布渲染以避免闪烁
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 1. 设置画布背景为白色
|
||||
this.originalCanvasBackground = this.canvas.backgroundColor;
|
||||
this.canvas.setBackgroundColor("#ffffff", () => {});
|
||||
|
||||
// 1. 设置画布背景为白色
|
||||
this.originalCanvasBackground = this.canvas.backgroundColor;
|
||||
this.canvas.setBackgroundColor('#ffffff', () => {});
|
||||
// 2. 查找图层结构
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 2. 查找图层结构
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
|
||||
throw new Error("缺少必要的图层结构");
|
||||
}
|
||||
|
||||
const normalLayer = normalLayers[0]; // 使用第一个普通图层
|
||||
|
||||
// 3. 保存原始状态
|
||||
this.originalBackgroundObject = backgroundLayer.fabricObject ? {
|
||||
...backgroundLayer.fabricObject.toObject(),
|
||||
ref: backgroundLayer.fabricObject
|
||||
} : null;
|
||||
|
||||
this.originalFixedObjects = fixedLayer.fabricObject
|
||||
? [fixedLayer.fabricObject]
|
||||
: [];
|
||||
|
||||
this.originalNormalObjects = normalLayer.fabricObjects
|
||||
? [...normalLayer.fabricObjects]
|
||||
: [];
|
||||
|
||||
// 保存普通图层透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
this.originalNormalOpacities.set(
|
||||
`${layer.id}_${obj.id || "unknown"}`,
|
||||
obj.opacity || 1
|
||||
);
|
||||
});
|
||||
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
|
||||
throw new Error("缺少必要的图层结构");
|
||||
}
|
||||
});
|
||||
|
||||
// 保存工具状态
|
||||
if (this.toolManager) {
|
||||
this.originalToolState = {
|
||||
currentTool: this.toolManager.getCurrentTool(),
|
||||
isRedGreenMode: this.toolManager.isRedGreenMode,
|
||||
};
|
||||
}
|
||||
const normalLayer = normalLayers[0]; // 使用第一个普通图层
|
||||
|
||||
// 4. 确保背景图层大小正确
|
||||
await this._setupBackgroundLayer(backgroundLayer);
|
||||
// 3. 保存原始状态
|
||||
this.originalBackgroundObject = backgroundLayer.fabricObject
|
||||
? {
|
||||
...backgroundLayer.fabricObject.toObject(),
|
||||
ref: backgroundLayer.fabricObject,
|
||||
}
|
||||
: null;
|
||||
|
||||
// 5. 并行加载两个图片
|
||||
const [clothingImg, redGreenImg] = await Promise.all([
|
||||
this._loadImage(this.clothingImageUrl),
|
||||
this._loadImage(this.redGreenImageUrl)
|
||||
]);
|
||||
this.originalFixedObjects = fixedLayer.fabricObject
|
||||
? [fixedLayer.fabricObject]
|
||||
: [];
|
||||
|
||||
// 6. 设置衣服底图到固定图层
|
||||
await this._setupClothingImage(clothingImg, fixedLayer);
|
||||
this.originalNormalObjects = normalLayer.fabricObjects
|
||||
? [...normalLayer.fabricObjects]
|
||||
: [];
|
||||
|
||||
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
|
||||
await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage);
|
||||
// 保存当前活动图层ID
|
||||
this.originalActiveLayerId = this.layerManager.getActiveLayerId();
|
||||
|
||||
// 8. 设置普通图层透明度
|
||||
this._setupNormalLayerOpacity(normalLayers);
|
||||
// 保存普通图层透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
this.originalNormalOpacities.set(
|
||||
`${layer.id}_${obj.id || "unknown"}`,
|
||||
obj.opacity || 1
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 9. 配置工具管理器
|
||||
this._setupToolManager();
|
||||
// 保存工具状态
|
||||
if (this.toolManager) {
|
||||
this.originalToolState = {
|
||||
currentTool: this.toolManager.getCurrentTool(),
|
||||
isRedGreenMode: this.toolManager.isRedGreenMode,
|
||||
};
|
||||
}
|
||||
|
||||
// 10. 重新启用渲染并执行一次性渲染
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
// 5. 并行加载两个图片
|
||||
const [clothingImg, redGreenImg] = await Promise.all([
|
||||
this._loadImage(this.clothingImageUrl),
|
||||
this._loadImage(this.redGreenImageUrl),
|
||||
]);
|
||||
|
||||
console.log("批量红绿图模式初始化完成", {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
画布背景: "白色",
|
||||
// 6. 设置衣服底图到固定图层
|
||||
await this._setupClothingImage(clothingImg, fixedLayer);
|
||||
|
||||
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
|
||||
await this._setupRedGreenImage(
|
||||
redGreenImg,
|
||||
normalLayer,
|
||||
this.clothingImage
|
||||
);
|
||||
|
||||
// 4. 确保背景图层大小和衣服地图大小一致
|
||||
await this._setupBackgroundLayer(backgroundLayer, this.clothingImage);
|
||||
|
||||
// 8. 设置普通图层透明度
|
||||
this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理
|
||||
|
||||
// 9. 创建新的空白图层并设置为活动图层
|
||||
this.newEmptyLayerId = await this._createAndActivateEmptyLayer();
|
||||
|
||||
// 设置普通图层的裁剪对象为衣服底图
|
||||
if (this.redGreenImage) {
|
||||
// const clipPathImg = this.redGreenImage;
|
||||
// clipPathImg.set({
|
||||
// absolutePositioned: true,
|
||||
// });
|
||||
this.redGreenImage.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
const activeLayer = this.layerManager.getActiveLayer();
|
||||
activeLayer.clippingMask = this.redGreenImage.toObject(["id"]);
|
||||
activeLayer.opacity = this.normalLayerOpacity;
|
||||
// activeLayer?.fabricObjects.forEach((obj) => {
|
||||
// obj.set({
|
||||
// clipPath: clipPathImg,
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
// 10. 配置工具管理器
|
||||
this._setupToolManager();
|
||||
|
||||
console.log("批量红绿图模式初始化完成", {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
画布背景: "白色",
|
||||
新建空图层ID: this.newEmptyLayerId,
|
||||
});
|
||||
|
||||
await this.layerManager.updateLayersObjectsInteractivity(false);
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -134,86 +173,125 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的空白图层并设置为活动图层
|
||||
* @returns {Promise<string>} 新创建的图层ID
|
||||
* @private
|
||||
*/
|
||||
async _createAndActivateEmptyLayer() {
|
||||
// 创建新的空白图层
|
||||
const newLayerName = "绘制图层";
|
||||
const newLayerId = this.layerManager.createLayer(
|
||||
newLayerName,
|
||||
LayerType.GROUP,
|
||||
{
|
||||
undoable: false,
|
||||
}
|
||||
);
|
||||
|
||||
// 设置为活动图层
|
||||
if (newLayerId) {
|
||||
this.layerManager.setActiveLayer(newLayerId);
|
||||
}
|
||||
|
||||
return newLayerId;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
try {
|
||||
// 禁用渲染
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
// 1. 恢复画布背景
|
||||
if (this.originalCanvasBackground !== null) {
|
||||
this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {});
|
||||
}
|
||||
|
||||
// 2. 恢复图层对象
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 移除当前添加的对象
|
||||
if (this.clothingImage) {
|
||||
this.canvas.remove(this.clothingImage);
|
||||
}
|
||||
if (this.redGreenImage) {
|
||||
this.canvas.remove(this.redGreenImage);
|
||||
}
|
||||
|
||||
// 恢复背景图层
|
||||
if (backgroundLayer && this.originalBackgroundObject) {
|
||||
if (this.originalBackgroundObject.ref) {
|
||||
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复固定图层
|
||||
if (fixedLayer) {
|
||||
fixedLayer.fabricObject = this.originalFixedObjects.length > 0
|
||||
? this.originalFixedObjects[0]
|
||||
: null;
|
||||
|
||||
if (fixedLayer.fabricObject) {
|
||||
this.canvas.add(fixedLayer.fabricObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复普通图层
|
||||
if (normalLayers.length > 0) {
|
||||
const normalLayer = normalLayers[0];
|
||||
normalLayer.fabricObjects = [...this.originalNormalObjects];
|
||||
this.originalNormalObjects.forEach((obj) => {
|
||||
this.canvas.add(obj);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 恢复透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
if (this.originalNormalOpacities.has(layer.id)) {
|
||||
layer.opacity = this.originalNormalOpacities.get(layer.id);
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 1. 恢复画布背景
|
||||
if (this.originalCanvasBackground !== null) {
|
||||
this.canvas.setBackgroundColor(
|
||||
this.originalCanvasBackground,
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
const key = `${layer.id}_${obj.id || "unknown"}`;
|
||||
if (this.originalNormalOpacities.has(key)) {
|
||||
obj.opacity = this.originalNormalOpacities.get(key);
|
||||
}
|
||||
// 2. 恢复图层对象
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 移除当前添加的对象
|
||||
if (this.clothingImage) {
|
||||
this.canvas.remove(this.clothingImage);
|
||||
}
|
||||
if (this.redGreenImage) {
|
||||
this.canvas.remove(this.redGreenImage);
|
||||
}
|
||||
|
||||
// 移除新创建的空白图层
|
||||
if (this.newEmptyLayerId) {
|
||||
const emptyLayerIndex = layers.findIndex(
|
||||
(layer) => layer.id === this.newEmptyLayerId
|
||||
);
|
||||
if (emptyLayerIndex !== -1) {
|
||||
layers.splice(emptyLayerIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复背景图层
|
||||
if (backgroundLayer && this.originalBackgroundObject) {
|
||||
if (this.originalBackgroundObject.ref) {
|
||||
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复固定图层
|
||||
if (fixedLayer) {
|
||||
fixedLayer.fabricObject =
|
||||
this.originalFixedObjects.length > 0
|
||||
? this.originalFixedObjects[0]
|
||||
: null;
|
||||
|
||||
if (fixedLayer.fabricObject) {
|
||||
this.canvas.add(fixedLayer.fabricObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复普通图层
|
||||
if (normalLayers.length > 0) {
|
||||
const normalLayer = normalLayers[0];
|
||||
normalLayer.fabricObjects = [...this.originalNormalObjects];
|
||||
this.originalNormalObjects.forEach((obj) => {
|
||||
this.canvas.add(obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 恢复工具状态
|
||||
if (this.toolManager && this.originalToolState) {
|
||||
this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode;
|
||||
if (this.originalToolState.currentTool) {
|
||||
this.toolManager.setTool(this.originalToolState.currentTool);
|
||||
// 3. 恢复透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
if (this.originalNormalOpacities.has(layer.id)) {
|
||||
layer.opacity = this.originalNormalOpacities.get(layer.id);
|
||||
}
|
||||
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
const key = `${layer.id}_${obj.id || "unknown"}`;
|
||||
if (this.originalNormalOpacities.has(key)) {
|
||||
obj.opacity = this.originalNormalOpacities.get(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复活动图层
|
||||
if (this.originalActiveLayerId) {
|
||||
this.layerManager.setActiveLayer(this.originalActiveLayerId);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 重新启用渲染
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
// 4. 恢复工具状态
|
||||
if (this.toolManager && this.originalToolState) {
|
||||
this.toolManager.isRedGreenMode =
|
||||
this.originalToolState.isRedGreenMode;
|
||||
if (this.originalToolState.currentTool) {
|
||||
this.toolManager.setTool(this.originalToolState.currentTool);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -226,35 +304,42 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
/**
|
||||
* 设置背景图层
|
||||
*/
|
||||
async _setupBackgroundLayer(backgroundLayer) {
|
||||
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
|
||||
let backgroundObject = backgroundLayer.fabricObject;
|
||||
const { object } = findObjectById(this.canvas, backgroundObject.id);
|
||||
|
||||
if (!backgroundObject) {
|
||||
if (!object) {
|
||||
// 创建白色背景矩形
|
||||
backgroundObject = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
fill: "#ffffff",
|
||||
object = new fabric.Rect({
|
||||
left: this.canvas.width / 2,
|
||||
top: this.canvas.height / 2,
|
||||
width: clothingImage.width,
|
||||
height: clothingImage.height,
|
||||
scaleX: clothingImage.scaleX,
|
||||
scaleY: clothingImage.scaleY,
|
||||
fill: "transparent", // 确保背景是透明的
|
||||
selectable: false,
|
||||
evented: false,
|
||||
isBackground: true,
|
||||
layerId: backgroundLayer.id,
|
||||
layerName: backgroundLayer.name,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
this.canvas.add(backgroundObject);
|
||||
this.canvas.sendToBack(backgroundObject);
|
||||
backgroundLayer.fabricObject = backgroundObject;
|
||||
this.canvas.add(object);
|
||||
this.canvas.sendToBack(object);
|
||||
backgroundLayer.fabricObject = object;
|
||||
} else {
|
||||
// 更新现有背景对象大小
|
||||
backgroundObject.set({
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
left: 0,
|
||||
top: 0,
|
||||
fill: "#ffffff", // 确保背景是白色
|
||||
object.set({
|
||||
width: clothingImage.width,
|
||||
height: clothingImage.height,
|
||||
scaleX: clothingImage.scaleX,
|
||||
scaleY: clothingImage.scaleY,
|
||||
left: this.canvas.width / 2,
|
||||
top: this.canvas.height / 2,
|
||||
fill: "transparent", // 确保背景是透明的
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -299,6 +384,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
evented: false,
|
||||
layerId: fixedLayer.id,
|
||||
layerName: fixedLayer.name,
|
||||
id: generateId("clothingImage"),
|
||||
});
|
||||
|
||||
// 清除固定图层原有内容
|
||||
@@ -332,6 +418,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
evented: false,
|
||||
layerId: normalLayer.id,
|
||||
layerName: normalLayer.name,
|
||||
id: generateId("redGreenImage"),
|
||||
});
|
||||
|
||||
// 清除普通图层原有内容
|
||||
@@ -341,6 +428,8 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
// 给img设置裁剪,裁剪图为衣服底图
|
||||
|
||||
// 添加到画布和普通图层
|
||||
this.canvas.add(img);
|
||||
normalLayer.fabricObjects = [img];
|
||||
@@ -354,13 +443,6 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
normalLayers.forEach((layer) => {
|
||||
// 设置图层透明度
|
||||
layer.opacity = this.normalLayerOpacity;
|
||||
|
||||
// 更新图层中所有对象的透明度
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
obj.opacity = this.normalLayerOpacity;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command, CompositeCommand } from "./Command.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { createLayer, LayerType } from "../utils/layerHelper.js";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<transition name="brush-control-canel-fade">
|
||||
<div v-if="isVisible" class="brush-control-panel">
|
||||
<!-- 笔刷大小控制 -->
|
||||
<VerticalSlider
|
||||
@@ -55,80 +55,89 @@
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
|
||||
<!-- 颜色选择器 - 仅在特定工具下显示 -->
|
||||
<div v-if="showColorPicker" class="color-picker-container">
|
||||
<label for="color-picker" class="current-color-label">
|
||||
<div
|
||||
class="current-color"
|
||||
:style="{ backgroundColor: brushColor }"
|
||||
></div>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color-picker"
|
||||
class="system-color-picker"
|
||||
v-model="customColor"
|
||||
@input="setBrushColor(customColor)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 透明度控制 - 仅在特定工具下显示 -->
|
||||
<VerticalSlider
|
||||
v-if="showOpacitySlider"
|
||||
v-model="brushOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:presets="opacityPresets"
|
||||
:memorized-values="memorizedOpacities"
|
||||
:is-percentage="true"
|
||||
custom-class="opacity-slider"
|
||||
:active-threshold="0.01"
|
||||
:step="0.01"
|
||||
v-model:showTooltip="showOpacityTooltip"
|
||||
@slide-start="handleOpacitySlideStart"
|
||||
@slide-end="handleOpacitySlideEnd"
|
||||
@click="showOpacityTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Opacity</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="opacity-preview">
|
||||
<div class="opacity-checker"></div>
|
||||
<!-- 1.这里加上过渡动画 颜色选择器 - 仅在特定工具下显示 -->
|
||||
<transition name="color-picker-fade" mode="out-in">
|
||||
<div
|
||||
v-if="showColorPicker"
|
||||
class="color-picker-container"
|
||||
key="color-picker"
|
||||
>
|
||||
<label for="color-picker" class="current-color-label">
|
||||
<div
|
||||
class="opacity-color"
|
||||
:style="{
|
||||
backgroundColor: brushColor,
|
||||
opacity: brushOpacity,
|
||||
}"
|
||||
class="current-color"
|
||||
:style="{ backgroundColor: brushColor }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">
|
||||
{{ Math.round(brushOpacity * 100) }}%
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color-picker"
|
||||
class="system-color-picker"
|
||||
v-model="customColor"
|
||||
@input="setBrushColor(customColor)"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 2.这里加上过渡动画 透明度控制 - 仅在特定工具下显示 -->
|
||||
<transition name="opacity-slider-fade" mode="out-in">
|
||||
<VerticalSlider
|
||||
v-if="showOpacitySlider"
|
||||
key="opacity-slider"
|
||||
v-model="brushOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:presets="opacityPresets"
|
||||
:memorized-values="memorizedOpacities"
|
||||
:is-percentage="true"
|
||||
custom-class="opacity-slider"
|
||||
:active-threshold="0.01"
|
||||
:step="0.01"
|
||||
v-model:showTooltip="showOpacityTooltip"
|
||||
@slide-start="handleOpacitySlideStart"
|
||||
@slide-end="handleOpacitySlideEnd"
|
||||
@click="showOpacityTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Opacity</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
class="control-btn add"
|
||||
v-if="!memorizedOpacities.includes(brushOpacity)"
|
||||
@click="memorizeOpacity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedOpacity"
|
||||
v-if="canRemoveOpacity"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<div class="opacity-preview">
|
||||
<div class="opacity-checker"></div>
|
||||
<div
|
||||
class="opacity-color"
|
||||
:style="{
|
||||
backgroundColor: brushColor,
|
||||
opacity: brushOpacity,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">
|
||||
{{ Math.round(brushOpacity * 100) }}%
|
||||
</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
class="control-btn add"
|
||||
v-if="!memorizedOpacities.includes(brushOpacity)"
|
||||
@click="memorizeOpacity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedOpacity"
|
||||
v-if="canRemoveOpacity"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -139,7 +148,6 @@ import { BrushStore } from "../store/BrushStore";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { inject } from "vue";
|
||||
import VerticalSlider from "./VerticalSlider.vue";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: {
|
||||
@@ -462,6 +470,13 @@ watch(
|
||||
color: #333;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
// 添加高度过渡动画
|
||||
transition: height 0.3s ease-out, min-height 0.3s ease-out;
|
||||
// overflow: hidden;
|
||||
|
||||
transform: translate3d(0, -50%, 0); // 确保使用3D变换以提高性能
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 笔刷大小预览相关样式
|
||||
@@ -660,27 +675,49 @@ watch(
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
.brush-control-canel-fade-enter-active,
|
||||
.brush-control-canel-fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
.brush-control-canel-fade-enter-from,
|
||||
.brush-control-canel-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) translateY(-50%);
|
||||
}
|
||||
|
||||
// 颜色选择器过渡动画
|
||||
.color-picker-fade-enter-active,
|
||||
.color-picker-fade-leave-active {
|
||||
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
|
||||
}
|
||||
.color-picker-fade-enter-from,
|
||||
.color-picker-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.95);
|
||||
}
|
||||
|
||||
// 透明度滑块过渡动画
|
||||
.opacity-slider-fade-enter-active,
|
||||
.opacity-slider-fade-leave-active {
|
||||
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
|
||||
}
|
||||
.opacity-slider-fade-enter-from,
|
||||
.opacity-slider-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-height: 600px) {
|
||||
.brush-control-panel {
|
||||
transform: translateY(-50%);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.brush-control-panel {
|
||||
left: 10px;
|
||||
// padding: 12px;
|
||||
padding: 12px 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,338 +1,350 @@
|
||||
<template>
|
||||
<div class="brush-panel">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-panel" @click.stop="">
|
||||
<div class="brush-panel-wrapper">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state
|
||||
.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
class="property-select"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
class="file-preview"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div
|
||||
class="texture-item upload-item"
|
||||
@click="triggerTextureUpload"
|
||||
>
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="color"
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-select"
|
||||
>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="file-preview" @click="handleFileSelect(prop.id)">
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div class="texture-item upload-item" @click="triggerTextureUpload">
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none;"
|
||||
class="property-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-input"
|
||||
/>
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="upload-btn"
|
||||
@click="handleFileSelect('texturePath')"
|
||||
>
|
||||
上传新材质
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="upload-btn" @click="handleFileSelect('texturePath')">
|
||||
上传新材质
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="presets-container">
|
||||
<div
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div class="presets-container">
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,10 +460,7 @@ function handleTextureSelect(textureId) {
|
||||
|
||||
// 触发纹理上传文件选择
|
||||
function triggerTextureUpload() {
|
||||
const fileInput = textureFileInput.value;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
handleFileSelect("texturePath");
|
||||
}
|
||||
|
||||
// 处理纹理文件上传
|
||||
@@ -461,15 +470,15 @@ async function handleTextureUpload(event) {
|
||||
|
||||
try {
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('请选择图片文件');
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("请选择图片文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小 (限制为 5MB)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
alert('文件大小不能超过 5MB');
|
||||
alert("文件大小不能超过 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,8 +487,8 @@ async function handleTextureUpload(event) {
|
||||
const brushManager = toolManager?.brushManager;
|
||||
|
||||
if (!texturePresetManager || !brushManager) {
|
||||
console.error('缺少必要的管理器实例');
|
||||
alert('系统错误:无法上传纹理');
|
||||
console.error("缺少必要的管理器实例");
|
||||
alert("系统错误:无法上传纹理");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,12 +502,11 @@ async function handleTextureUpload(event) {
|
||||
});
|
||||
|
||||
await commandManager.execute(command);
|
||||
|
||||
|
||||
// 清空文件输入,允许重复上传同一文件
|
||||
event.target.value = '';
|
||||
|
||||
event.target.value = "";
|
||||
} catch (error) {
|
||||
console.error('纹理上传失败:', error);
|
||||
console.error("纹理上传失败:", error);
|
||||
alert(`上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -673,29 +681,44 @@ onMounted(() => {
|
||||
const brushStore = BrushStore;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.brush-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 62px;
|
||||
padding: 0;
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 85vh;
|
||||
width: 30%;
|
||||
/* overflow-y: auto; */
|
||||
min-width: 280px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
position: relative;
|
||||
animation: panelFadeIn 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
z-index: 1000; /* 确保面板在最上层 */
|
||||
|
||||
.brush-panel-wrapper {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-height: 85vh; /* 限制最大高度 */
|
||||
/*优化ios上的滚动效果*/
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加指向整个面板的倒三角 */
|
||||
.brush-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
top: -9px;
|
||||
right: 58px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
@@ -716,12 +739,6 @@ const brushStore = BrushStore;
|
||||
}
|
||||
}
|
||||
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brush-section {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
@@ -746,9 +763,10 @@ const brushStore = BrushStore;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
}
|
||||
|
||||
.section-header::after {
|
||||
/* .section-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@@ -757,18 +775,18 @@ const brushStore = BrushStore;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #999; /* 更柔和的颜色 */
|
||||
border-top: 6px solid #999;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-header:hover {
|
||||
/* .section-header:hover {
|
||||
background-color: rgba(248, 249, 250, 1);
|
||||
}
|
||||
|
||||
.section-header:hover::after {
|
||||
border-top-color: #666;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
@@ -1114,6 +1132,12 @@ const brushStore = BrushStore;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.property-select {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-select:focus {
|
||||
border-color: #4285f4;
|
||||
outline: none;
|
||||
|
||||
@@ -17,6 +17,7 @@ provide("brushStore", BrushStore);
|
||||
|
||||
const toolManager = inject("toolManager");
|
||||
const layerManager = inject("layerManager");
|
||||
const isShowLayerPanel = inject("isShowLayerPanel", ref(false));
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
@@ -24,6 +25,7 @@ const props = defineProps({
|
||||
canvasHeight: Number,
|
||||
canvasColor: String,
|
||||
brushSize: Number,
|
||||
enabledRedGreenMode: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -40,9 +42,9 @@ const showBrushPanel = ref(false);
|
||||
const brushPanelRef = ref(null);
|
||||
|
||||
// 计算属性
|
||||
const shouldShowBrushSettings = computed(() => {
|
||||
return props.activeTool === OperationType.DRAW;
|
||||
});
|
||||
// const shouldShowBrushSettings = computed(() => {
|
||||
// return props.activeTool === OperationType.DRAW;
|
||||
// });
|
||||
|
||||
function updateCanvasSize() {
|
||||
if (!layerManager) {
|
||||
@@ -86,6 +88,11 @@ function updateCanvasColor() {
|
||||
|
||||
// 切换笔刷面板显示状态
|
||||
function toggleBrushPanel() {
|
||||
// 如果笔刷没有激活 则激活笔刷工具
|
||||
if (toolManager?.activeTool !== OperationType.DRAW) {
|
||||
toolManager.setToolWithCommand(OperationType.DRAW);
|
||||
}
|
||||
|
||||
showBrushPanel.value = !showBrushPanel.value;
|
||||
}
|
||||
|
||||
@@ -176,16 +183,37 @@ function syncBrushStoreToManager() {
|
||||
|
||||
// 点击外部时关闭笔刷面板
|
||||
function handleClickOutside(event) {
|
||||
if (
|
||||
showBrushPanel.value &&
|
||||
brushPanelRef.value &&
|
||||
!brushPanelRef.value.contains(event.target) &&
|
||||
!event.target.closest(".brush-selector")
|
||||
) {
|
||||
showBrushPanel.value = false;
|
||||
// if (isShowLayerPanel.value) {
|
||||
// // 如果点击的是图层面板或其内部元素,则不关闭
|
||||
// if (event.target.closest(".layers-panel")) {
|
||||
// return;
|
||||
// }
|
||||
// // 关闭图层面板
|
||||
// isShowLayerPanel.value = false;
|
||||
// }
|
||||
|
||||
if (showBrushPanel.value) {
|
||||
// 检查是否点击了笔刷选择器按钮
|
||||
if (event.target.closest(".brush-selector")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了笔刷面板或其内部元素
|
||||
if (event.target.closest(".brush-panel")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果都不是,则关闭面板
|
||||
if (showBrushPanel.value) {
|
||||
showBrushPanel.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showLayerPanel() {
|
||||
isShowLayerPanel.value = !isShowLayerPanel.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取工具管理器和笔刷管理器
|
||||
const brushManager = toolManager?.brushManager;
|
||||
@@ -229,94 +257,51 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="canvas-header">
|
||||
<span class="canvas-title">Canvas</span>
|
||||
|
||||
<!-- 默认设置 -->
|
||||
<div
|
||||
v-if="
|
||||
<div class="canvas-header-wrapper">
|
||||
<span class="canvas-title">Canvas</span>
|
||||
<!-- 默认设置 -->
|
||||
<!-- v-if="
|
||||
!activeTool ||
|
||||
activeTool === OperationType.SELECT ||
|
||||
activeTool === OperationType.PAN
|
||||
"
|
||||
class="canvas-settings"
|
||||
>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<div class="color-picker-wrapper">
|
||||
" -->
|
||||
<div class="canvas-settings" v-if="!props.enabledRedGreenMode">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="color"
|
||||
:value="canvasColor"
|
||||
class="color-picker"
|
||||
@input="$emit('update:canvasColor', $event.target.value)"
|
||||
@change="updateCanvasColor"
|
||||
type="text"
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
:value="canvasColor"
|
||||
class="color-picker"
|
||||
@input="$emit('update:canvasColor', $event.target.value)"
|
||||
@change="updateCanvasColor"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div v-if="shouldShowBrushSettings" class="canvas-settings">
|
||||
<!-- 简化的笔刷控制UI -->
|
||||
<!-- <div class="setting-group">
|
||||
<span class="setting-label">大小:</span>
|
||||
<input
|
||||
type="range"
|
||||
:value="BrushStore.state.size"
|
||||
min="0.5"
|
||||
max="100"
|
||||
step="0.5"
|
||||
class="size-slider"
|
||||
@input="handleBrushSizeChange"
|
||||
/>
|
||||
<span class="size-value">{{ BrushStore.state.size }}px</span>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">笔刷:</span>
|
||||
<div class="brush-selector" @click="toggleBrushPanel">
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="{
|
||||
backgroundColor: BrushStore.state.color,
|
||||
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
||||
opacity: BrushStore.state.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="brush-dropdown">▼</span>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷面板 -->
|
||||
<div
|
||||
v-if="showBrushPanel"
|
||||
class="brush-panel-container"
|
||||
ref="brushPanelRef"
|
||||
>
|
||||
<BrushPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">颜色:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
@@ -327,64 +312,71 @@ onMounted(() => {
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本工具设置 -->
|
||||
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Font:</span>
|
||||
<select class="font-select">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Size:</span>
|
||||
<input
|
||||
type="number"
|
||||
class="setting-input"
|
||||
value="16"
|
||||
min="8"
|
||||
max="72"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color" class="color-picker" value="#000000" />
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传工具设置 -->
|
||||
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Upload Type:</span>
|
||||
<select class="setting-select">
|
||||
<option value="image">Image</option>
|
||||
<option value="vector">Vector Graphics</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 导出设置 -->
|
||||
<div class="setting-group export-group">
|
||||
<!-- <div class="setting-group export-group">
|
||||
<span class="export-model-select">exportModel.select:</span>
|
||||
<span class="export-model-dropdown">▼</span>
|
||||
</div> -->
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
|
||||
<div
|
||||
class="btn"
|
||||
:class="{ active: showBrushPanel }"
|
||||
@click="toggleBrushPanel"
|
||||
>
|
||||
<!-- <span class="setting-label">笔刷:</span>/ -->
|
||||
<div class="brush-selector">
|
||||
<SvgIcon name="CBrushTop" size="22"></SvgIcon>
|
||||
<!-- <div
|
||||
class="brush-preview"
|
||||
:style="{
|
||||
backgroundColor: BrushStore.state.color,
|
||||
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
||||
opacity: BrushStore.state.opacity,
|
||||
}"
|
||||
></div> -->
|
||||
<!-- <span class="brush-dropdown">▼</span> -->
|
||||
</div>
|
||||
<!-- 笔刷面板 -->
|
||||
<div
|
||||
v-if="showBrushPanel"
|
||||
class="brush-panel-container"
|
||||
ref="brushPanelRef"
|
||||
>
|
||||
<Teleport to="body">
|
||||
<BrushPanel />
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="btn"
|
||||
:class="{ active: isShowLayerPanel }"
|
||||
@click="showLayerPanel"
|
||||
>
|
||||
<SvgIcon name="CLayout" size="26"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.canvas-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
user-select: none;
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
.canvas-header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-title {
|
||||
@@ -393,19 +385,44 @@ onMounted(() => {
|
||||
margin-right: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.canvas-title::before {
|
||||
content: "⟳";
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
// &:before {
|
||||
// // /* content: "⟳";
|
||||
// // margin-right: 5px;
|
||||
// // font-size: 14px; */
|
||||
// }
|
||||
}
|
||||
|
||||
.canvas-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
color: #213547;
|
||||
.btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
// color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gap-20 {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
@@ -476,15 +493,15 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.brush-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
width: 80px;
|
||||
justify-content: space-between;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// border: 1px solid #ddd;
|
||||
// border-radius: 4px;
|
||||
// padding: 5px;
|
||||
// cursor: pointer;
|
||||
// background-color: white;
|
||||
// width: 80px;
|
||||
// justify-content: space-between;
|
||||
}
|
||||
|
||||
.brush-preview {
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "select"]);
|
||||
|
||||
const menuRef = ref(null);
|
||||
const adjustedPosition = ref({ x: 0, y: 0 });
|
||||
const hoveredItem = ref(null);
|
||||
const submenuPositions = ref(new Map());
|
||||
const hideTimer = ref(null); // 添加隐藏定时器
|
||||
|
||||
// 计算菜单位置,处理边界问题
|
||||
const calculatePosition = () => {
|
||||
if (!menuRef.value || !props.visible) return;
|
||||
|
||||
const menu = menuRef.value;
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let x = props.position.x;
|
||||
let y = props.position.y;
|
||||
|
||||
// 右边界检测
|
||||
if (x + menuRect.width > windowWidth - 10) {
|
||||
x = x - menuRect.width;
|
||||
}
|
||||
|
||||
// 底边界检测
|
||||
if (y + menuRect.height > windowHeight - 10) {
|
||||
y = windowHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
}
|
||||
|
||||
// 顶边界检测
|
||||
if (y < 10) {
|
||||
y = 10;
|
||||
}
|
||||
|
||||
adjustedPosition.value = { x, y };
|
||||
};
|
||||
|
||||
// 计算子菜单位置
|
||||
const calculateSubmenuPosition = (itemElement, itemIndex) => {
|
||||
if (!itemElement || !menuRef.value) return { x: 0, y: 0, direction: "right" };
|
||||
|
||||
const itemRect = itemElement.getBoundingClientRect();
|
||||
const menuRect = menuRef.value.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// 预估子菜单宽度(可以根据实际情况调整)
|
||||
const submenuWidth = 200;
|
||||
const submenuHeight = 300; // 预估高度
|
||||
|
||||
let x = itemRect.right + 4;
|
||||
// 直接使用菜单项相对于主菜单容器的偏移量
|
||||
let y = itemElement.offsetTop;
|
||||
let direction = "right";
|
||||
|
||||
// 右边界检测,如果右侧空间不足,显示在左侧
|
||||
if (x + submenuWidth > windowWidth - 10) {
|
||||
x = itemRect.left - submenuWidth - 4;
|
||||
direction = "left";
|
||||
}
|
||||
|
||||
// 底边界检测 - 基于子菜单的绝对位置检查
|
||||
const absoluteSubmenuBottom = itemRect.top + submenuHeight;
|
||||
if (absoluteSubmenuBottom > windowHeight - 10) {
|
||||
// 计算可用的最大Y位置(相对于主菜单)
|
||||
const maxAbsoluteY = windowHeight - submenuHeight - 10;
|
||||
const maxRelativeY = maxAbsoluteY - menuRect.top;
|
||||
y = Math.max(0, maxRelativeY);
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
direction = "right";
|
||||
}
|
||||
|
||||
// 确保 y 不为负数
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
|
||||
y = 0;
|
||||
|
||||
const position = { x, y, direction };
|
||||
submenuPositions.value.set(itemIndex, position);
|
||||
return position;
|
||||
};
|
||||
|
||||
// 清除隐藏定时器
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 显示子菜单
|
||||
const showSubmenu = (item, index, element) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
nextTick(() => {
|
||||
calculateSubmenuPosition(element, index);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏子菜单(延迟)
|
||||
const hideSubmenu = (index) => {
|
||||
clearHideTimer();
|
||||
hideTimer.value = setTimeout(() => {
|
||||
if (hoveredItem.value === index) {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// 处理鼠标进入菜单项
|
||||
const handleItemMouseEnter = (item, index, event) => {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
};
|
||||
|
||||
// 处理鼠标在菜单项内移动
|
||||
const handleItemMouseMove = (item, index, event) => {
|
||||
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
|
||||
if (
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem.value !== index
|
||||
) {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标离开菜单项
|
||||
const handleItemMouseLeave = (item, index) => {
|
||||
// 只有当有子菜单时才延迟隐藏
|
||||
if (item.children && item.children.length > 0) {
|
||||
hideSubmenu(index);
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入子菜单
|
||||
const handleSubmenuMouseEnter = (index) => {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
};
|
||||
|
||||
// 处理鼠标离开子菜单
|
||||
const handleSubmenuMouseLeave = (index) => {
|
||||
hideSubmenu(index);
|
||||
};
|
||||
|
||||
// 监听可见性和位置变化
|
||||
watch([() => props.visible, () => props.position], () => {
|
||||
if (props.visible) {
|
||||
nextTick(() => {
|
||||
calculatePosition();
|
||||
});
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
submenuPositions.value.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理菜单项点击
|
||||
const handleItemClick = (item, index) => {
|
||||
if (item.disabled || item.type === "divider") return;
|
||||
|
||||
// 如果有子菜单,不关闭菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("select", item, index);
|
||||
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理子菜单项点击
|
||||
const handleSubItemClick = (subItem, parentIndex, subIndex) => {
|
||||
if (subItem.disabled || subItem.type === "divider") return;
|
||||
|
||||
emit("select", subItem, `${parentIndex}-${subIndex}`);
|
||||
|
||||
if (subItem.action) {
|
||||
subItem.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理外部点击关闭
|
||||
const handleOutsideClick = (event) => {
|
||||
if (menuRef.value && !menuRef.value.contains(event.target)) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理 ESC 键关闭
|
||||
const handleEscKey = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleOutsideClick, true);
|
||||
document.addEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleOutsideClick, true);
|
||||
document.removeEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition name="context-menu">
|
||||
<div
|
||||
v-if="visible"
|
||||
ref="menuRef"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
top: `${adjustedPosition.y}px`,
|
||||
left: `${adjustedPosition.x}px`,
|
||||
}"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="item.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: item.disabled,
|
||||
danger: item.danger,
|
||||
'has-children': item.children && item.children.length > 0,
|
||||
hovered: hoveredItem === index,
|
||||
}"
|
||||
@click="handleItemClick(item, index)"
|
||||
@mouseenter="handleItemMouseEnter(item, index, $event)"
|
||||
@mousemove="handleItemMouseMove(item, index, $event)"
|
||||
@mouseleave="handleItemMouseLeave(item, index)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="item.icon">
|
||||
<SvgIcon
|
||||
:name="item.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: item.inverIcon ? `rotate(90deg)` : 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ item.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="item.shortcut">
|
||||
{{ item.shortcut }}
|
||||
</span>
|
||||
<span
|
||||
class="context-menu-arrow"
|
||||
v-if="item.children && item.children.length > 0"
|
||||
>
|
||||
<SvgIcon name="CRight" size="12" />
|
||||
</span>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<transition name="context-submenu">
|
||||
<div
|
||||
v-if="
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem === index
|
||||
"
|
||||
class="context-submenu"
|
||||
:class="{
|
||||
'submenu-left':
|
||||
submenuPositions.get(index)?.direction === 'left',
|
||||
}"
|
||||
@mouseenter="handleSubmenuMouseEnter(index)"
|
||||
@mouseleave="handleSubmenuMouseLeave"
|
||||
>
|
||||
<template
|
||||
v-for="(subItem, subIndex) in item.children"
|
||||
:key="subIndex"
|
||||
>
|
||||
<!-- 子菜单分隔线 -->
|
||||
<div
|
||||
v-if="subItem.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 子菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: subItem.disabled,
|
||||
danger: subItem.danger,
|
||||
}"
|
||||
@click="handleSubItemClick(subItem, index, subIndex)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="subItem.icon">
|
||||
<SvgIcon
|
||||
:name="subItem.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: subItem.inverIcon
|
||||
? `rotate(90deg)`
|
||||
: 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ subItem.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="subItem.shortcut">
|
||||
{{ subItem.shortcut }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import "./contextMenu.less";
|
||||
</style>
|
||||
@@ -0,0 +1,514 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, computed, inject } from "vue";
|
||||
import { Checkbox } from "ant-design-vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
import { isGroupLayer } from "../../utils/layerHelper";
|
||||
|
||||
// 设置组件名称,用于递归渲染
|
||||
defineOptions({
|
||||
name: "LayerItem",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
layer: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isChild: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isMultiSelectMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editingName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
thumbnailUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
isHidenDragHandle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expandedGroupIds: {
|
||||
type: Set,
|
||||
default: () => new Set(),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"click",
|
||||
"double-click",
|
||||
"context-menu",
|
||||
"checkbox-change",
|
||||
"toggle-visibility",
|
||||
"toggle-lock",
|
||||
"delete",
|
||||
"edit-confirm",
|
||||
"edit-cancel",
|
||||
"edit-keydown",
|
||||
"touch-start",
|
||||
"touch-move",
|
||||
"touch-end",
|
||||
"child-layers-sort",
|
||||
"update-child-layers",
|
||||
"toggle-group-expanded",
|
||||
// 新增子图层专用事件
|
||||
"toggle-child-visibility",
|
||||
"toggle-child-lock",
|
||||
"delete-child",
|
||||
"rename-child",
|
||||
// v-model相关事件
|
||||
"update:editingName",
|
||||
]);
|
||||
|
||||
const layerManager = inject("layerManager", null);
|
||||
|
||||
// 计算属性
|
||||
const isGroupLayerType = computed(() => {
|
||||
return isGroupLayer(props.layer);
|
||||
});
|
||||
|
||||
// 计算属性:检查组图层是否展开
|
||||
const isGroupExpanded = computed(() => {
|
||||
return props.expandedGroupIds.has(props.layer.id);
|
||||
});
|
||||
|
||||
// 获取子图层
|
||||
const childLayers = computed(() => {
|
||||
if (!isGroupLayerType.value) return [];
|
||||
|
||||
// 优先使用 layer.children 属性
|
||||
if (props.layer.children && Array.isArray(props.layer.children)) {
|
||||
return props.layer.children;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
// 切换组图层展开/收起状态
|
||||
const toggleGroupExpanded = () => {
|
||||
emit("toggle-group-expanded", props.layer.id);
|
||||
};
|
||||
|
||||
// 获取图层类型图标
|
||||
function getLayerTypeIcon(layer) {
|
||||
if (!layer) return "🖼️";
|
||||
|
||||
if (isGroupLayer(layer)) {
|
||||
return "📁";
|
||||
}
|
||||
|
||||
if (layer.fabricObject) {
|
||||
switch (layer.fabricObject.type) {
|
||||
case "image":
|
||||
return "🖼️";
|
||||
case "text":
|
||||
return "📝";
|
||||
case "rect":
|
||||
return "▢";
|
||||
case "circle":
|
||||
return "⬤";
|
||||
case "path":
|
||||
return "✎";
|
||||
default:
|
||||
return "⬤";
|
||||
}
|
||||
}
|
||||
|
||||
return "🖼️";
|
||||
}
|
||||
|
||||
function getLayerTypeText(layerType) {
|
||||
const typeMap = {
|
||||
EMPTY: "空图层",
|
||||
TEXT: "文本",
|
||||
IMAGE: "图片",
|
||||
SHAPE: "形状",
|
||||
GROUP: "组合",
|
||||
BACKGROUND: "背景",
|
||||
FIXED: "固定",
|
||||
};
|
||||
|
||||
return typeMap[layerType] || "未知";
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleClick(event) {
|
||||
emit("click", props.layer, event);
|
||||
}
|
||||
|
||||
function handleDoubleClick(event) {
|
||||
emit("double-click", props.layer, event);
|
||||
}
|
||||
|
||||
function handleContextMenu(event) {
|
||||
emit("context-menu", event, props.layer);
|
||||
}
|
||||
|
||||
function handleCheckboxChange(event) {
|
||||
emit("checkbox-change", props.layer.id, event);
|
||||
}
|
||||
|
||||
function handleToggleVisibility() {
|
||||
if (props.isChild) {
|
||||
// 子图层需要传递父图层ID - 从父级组件获取
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
emit("toggle-child-visibility", props.layer.id, parentId);
|
||||
} else {
|
||||
// 一级图层
|
||||
emit("toggle-visibility", props.layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleLock() {
|
||||
if (props.isChild) {
|
||||
// 子图层需要传递父图层ID - 从父级组件获取
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
emit("toggle-child-lock", props.layer.id, parentId);
|
||||
} else {
|
||||
// 一级图层
|
||||
emit("toggle-lock", props.layer);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!props.canDelete) {
|
||||
console.warn("当前图层无法删除:", props.layer.id);
|
||||
return;
|
||||
}
|
||||
if (props.isChild) {
|
||||
// 子图层删除:需要传递父图层ID
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
if (parentId) {
|
||||
emit("delete-child", props.layer.id, parentId);
|
||||
} else {
|
||||
console.warn("无法找到子图层的父图层ID:", props.layer.id);
|
||||
}
|
||||
} else {
|
||||
// 一级图层删除
|
||||
emit("delete", props.layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditConfirm() {
|
||||
if (props.isChild) {
|
||||
// 子图层重命名:需要传递父图层ID
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
if (props.editingName && props.editingName.trim() && parentId) {
|
||||
emit("rename-child", props.layer.id, parentId, props.editingName.trim());
|
||||
} else if (!parentId) {
|
||||
console.warn("无法找到子图层的父图层ID:", props.layer.id);
|
||||
}
|
||||
// 发送编辑取消事件,清理编辑状态
|
||||
emit("edit-cancel");
|
||||
} else {
|
||||
// 一级图层重命名
|
||||
emit("edit-confirm");
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditCancel() {
|
||||
emit("edit-cancel");
|
||||
}
|
||||
|
||||
function handleEditKeydown(event) {
|
||||
emit("edit-keydown", event); // 修复事件名称,从 "edit-keyboard" 改为 "edit-keydown"
|
||||
}
|
||||
|
||||
function handleTouchStart(event) {
|
||||
emit("touch-start", event, props.layer);
|
||||
}
|
||||
|
||||
function handleTouchMove(event) {
|
||||
emit("touch-move", event);
|
||||
}
|
||||
|
||||
function handleTouchEnd(event) {
|
||||
emit("touch-end", event);
|
||||
}
|
||||
|
||||
function handleUpdateChildLayers(newChildren) {
|
||||
// 更新当前组图层的children数组
|
||||
console.log(
|
||||
"更新子图层顺序:",
|
||||
"父图层ID:",
|
||||
props.layer.id,
|
||||
"新顺序:",
|
||||
newChildren
|
||||
);
|
||||
emit("update-child-layers", props.layer.id, newChildren);
|
||||
}
|
||||
|
||||
// 子图层递归事件处理
|
||||
function handleChildClick(childLayer, event) {
|
||||
emit("click", childLayer, event);
|
||||
}
|
||||
|
||||
function handleChildDoubleClick(childLayer, event) {
|
||||
emit("double-click", childLayer, event);
|
||||
}
|
||||
|
||||
function handleChildContextMenu(event, childLayer) {
|
||||
emit("context-menu", event, childLayer);
|
||||
}
|
||||
|
||||
function handleChildToggleVisibility(childLayerId) {
|
||||
emit("toggle-visibility", childLayerId);
|
||||
}
|
||||
|
||||
function handleChildToggleLock(childLayer) {
|
||||
emit("toggle-lock", childLayer);
|
||||
}
|
||||
|
||||
// 动画钩子函数
|
||||
function onEnter(el) {
|
||||
// 设置初始状态
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
el.style.paddingTop = "0";
|
||||
el.style.paddingBottom = "0";
|
||||
el.style.marginTop = "0";
|
||||
el.style.marginBottom = "0";
|
||||
el.style.overflow = "hidden";
|
||||
|
||||
// 强制重排
|
||||
el.offsetHeight;
|
||||
|
||||
// 获取最终高度
|
||||
el.style.height = "auto";
|
||||
const finalHeight = el.scrollHeight;
|
||||
el.style.height = "0";
|
||||
|
||||
// 执行动画
|
||||
requestAnimationFrame(() => {
|
||||
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
el.style.height = finalHeight + "px";
|
||||
el.style.opacity = "1";
|
||||
el.style.paddingTop = "";
|
||||
el.style.paddingBottom = "";
|
||||
el.style.marginTop = "";
|
||||
el.style.marginBottom = "";
|
||||
});
|
||||
}
|
||||
|
||||
function onLeave(el) {
|
||||
// 设置当前高度
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
el.style.overflow = "hidden";
|
||||
|
||||
// 强制重排
|
||||
el.offsetHeight;
|
||||
|
||||
// 执行收起动画
|
||||
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
el.style.paddingTop = "0";
|
||||
el.style.paddingBottom = "0";
|
||||
el.style.marginTop = "0";
|
||||
el.style.marginBottom = "0";
|
||||
}
|
||||
|
||||
// 查找父图层ID的辅助方法 - 增强版本
|
||||
function findParentLayerId() {
|
||||
// 首先检查 layer 对象是否已经有 parentId 属性
|
||||
if (props.layer.parentId) {
|
||||
return props.layer.parentId;
|
||||
}
|
||||
|
||||
// 如果没有,尝试从 layerManager 中查找
|
||||
if (layerManager && layerManager.layers) {
|
||||
for (const layer of layerManager.layers.value) {
|
||||
if (
|
||||
layer.children &&
|
||||
Array.isArray(layer.children) &&
|
||||
layer.children.some((child) => child.id === props.layer.id)
|
||||
) {
|
||||
return layer.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn("无法找到图层的父图层:", props.layer.id);
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 主图层项 -->
|
||||
<div
|
||||
:class="[
|
||||
'layer-item',
|
||||
{
|
||||
'child-layer': isChild,
|
||||
active: isActive,
|
||||
selected: isSelected,
|
||||
'group-layer': isGroupLayerType,
|
||||
editing: isEditing,
|
||||
'multi-select-mode': isMultiSelectMode,
|
||||
invisible: !layer.visible,
|
||||
locked: layer.locked,
|
||||
'fixed-layer': layer.isBackground || layer.isFixed,
|
||||
},
|
||||
]"
|
||||
@click="handleClick"
|
||||
@dblclick="handleDoubleClick"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="layer-drag-handle" :title="$t('拖拽排序')">
|
||||
<SvgIcon
|
||||
v-if="!isHidenDragHandle"
|
||||
:name="isChild ? 'CSort' : 'CSort'"
|
||||
:size="16"
|
||||
></SvgIcon>
|
||||
</div>
|
||||
|
||||
<!-- 图层头部 -->
|
||||
<div class="layer-header">
|
||||
<!-- 多选复选框 -->
|
||||
<div
|
||||
v-if="isMultiSelectMode && !isChild"
|
||||
class="layer-checkbox"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
|
||||
</div>
|
||||
|
||||
<!-- 图层预览图标 -->
|
||||
<div class="layer-review">
|
||||
<img
|
||||
v-if="thumbnailUrl"
|
||||
:src="thumbnailUrl"
|
||||
class="layer-thumbnail"
|
||||
:alt="$t('图层预览')"
|
||||
/>
|
||||
<span v-else class="layer-type-icon">{{
|
||||
getLayerTypeIcon(layer)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 图层名称 -->
|
||||
<div class="layer-name-container" :title="layer.name">
|
||||
<div class="layer-name-wrapper">
|
||||
<span v-if="!isEditing" class="layer-name text-ellipsis">
|
||||
{{ layer.name }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
:value="editingName"
|
||||
:data-layer-id="layer.id"
|
||||
:data-child-layer-id="isChild ? layer.id : undefined"
|
||||
class="layer-name-input"
|
||||
@blur="handleEditConfirm"
|
||||
@keydown="handleEditKeydown"
|
||||
@click.stop
|
||||
@input="$emit('update:editingName', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图层操作按钮 -->
|
||||
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
|
||||
<!-- 可见性切换 -->
|
||||
<div
|
||||
class="visibility-btn"
|
||||
@click.stop="handleToggleVisibility"
|
||||
:title="$t('显示/隐藏图层')"
|
||||
>
|
||||
<SvgIcon v-if="layer.visible" name="CEye" :size="16"></SvgIcon>
|
||||
<SvgIcon v-else name="CUnEye" :size="16"></SvgIcon>
|
||||
</div>
|
||||
|
||||
<!-- 锁定状态 -->
|
||||
<span
|
||||
v-if="layer.locked"
|
||||
class="status-icon locked"
|
||||
:class="{ disabled: layer.isBackground || layer.isFixed }"
|
||||
:title="$t('锁定')"
|
||||
@click.stop="handleToggleLock"
|
||||
>
|
||||
<SvgIcon name="CLock" :size="18"></SvgIcon>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="status-icon"
|
||||
:title="$t('未锁定')"
|
||||
@click.stop="handleToggleLock"
|
||||
>
|
||||
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
|
||||
</span>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<div
|
||||
class="delete-btn"
|
||||
:class="{ disabled: !canDelete }"
|
||||
:title="$t('删除图层')"
|
||||
@click.stop="handleDelete"
|
||||
>
|
||||
<SvgIcon name="CDelete" size="14"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组图层展开/收起图标 -->
|
||||
<div
|
||||
v-if="isGroupLayerType && !isChild"
|
||||
class="group-expand-icon"
|
||||
@click.stop="toggleGroupExpanded"
|
||||
@dblclick.stop=""
|
||||
:title="isGroupExpanded ? $t('收起组') : $t('展开组')"
|
||||
>
|
||||
<SvgIcon
|
||||
name="CRight"
|
||||
:size="12"
|
||||
:style="{
|
||||
transform: isGroupExpanded ? 'rotate(45deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图层状态指示器 -->
|
||||
<!-- <div v-if="!isChild" class="layer-status">
|
||||
<span
|
||||
v-if="isGroupLayerType"
|
||||
class="status-icon group"
|
||||
:title="$t('组图层')"
|
||||
>
|
||||
📁
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import "./layersPanel.less";
|
||||
</style>
|
||||
@@ -0,0 +1,328 @@
|
||||
<script setup>
|
||||
import { computed, inject } from "vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import LayerItem from "./LayerItem.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "LayersList",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
layers: Array,
|
||||
activeLayerId: String,
|
||||
sortableRootLayers: Array,
|
||||
selectedLayerIds: Array,
|
||||
isMultiSelectMode: Boolean,
|
||||
editingLayerId: String,
|
||||
editingLayerName: String,
|
||||
thumbnailManager: Object,
|
||||
groupName: String,
|
||||
expandedGroupIds: Set, // 新增:展开状态集合
|
||||
isChild: Boolean,
|
||||
parentLayerId: String, // 新增:父图层ID
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"layer-click",
|
||||
"layer-double-click",
|
||||
"context-menu",
|
||||
"checkbox-change",
|
||||
"toggle-visibility",
|
||||
"toggle-lock",
|
||||
"delete",
|
||||
"edit-confirm",
|
||||
"edit-cancel",
|
||||
"edit-keydown",
|
||||
"touch-start",
|
||||
"touch-move",
|
||||
"touch-end",
|
||||
"update:editing-name",
|
||||
"root-layers-sort",
|
||||
"child-layers-sort",
|
||||
"select-child-layer",
|
||||
"start-child-layer-edit",
|
||||
"child-context-menu",
|
||||
"finish-child-layer-edit",
|
||||
"cancel-child-layer-edit",
|
||||
"child-layer-edit-keydown",
|
||||
"toggle-group-expanded",
|
||||
// 新增子图层专用事件
|
||||
"toggle-child-visibility",
|
||||
"toggle-child-lock",
|
||||
"delete-child",
|
||||
"rename-child",
|
||||
]);
|
||||
|
||||
// 检查图层是否被选中
|
||||
const isLayerSelected = (layerId) => {
|
||||
return props.selectedLayerIds.includes(layerId);
|
||||
};
|
||||
|
||||
// 获取图层缩略图URL
|
||||
function getLayerThumbnail(layerId) {
|
||||
if (props.thumbnailManager) {
|
||||
return props.thumbnailManager.getLayerThumbnail(layerId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 事件转发方法
|
||||
const forwardEvent = (eventName, ...args) => {
|
||||
emit(eventName, ...args);
|
||||
};
|
||||
|
||||
// 处理根级图层拖拽排序
|
||||
const handleRootLayersSort = (event) => {
|
||||
if (props.isChild) {
|
||||
// 子图层事件处理
|
||||
// 确保排序只影响当前组图层的children,而不是全局layers
|
||||
emit(
|
||||
"child-layers-sort",
|
||||
event,
|
||||
props.sortableRootLayers,
|
||||
props.parentLayerId
|
||||
);
|
||||
} else {
|
||||
emit("root-layers-sort", event);
|
||||
}
|
||||
};
|
||||
|
||||
const canDeleteComputed = computed(() => {
|
||||
// 如果是子图层,检查父图层是否可以删除
|
||||
if (props.isChild) {
|
||||
const parentLayer = props.layers.find(
|
||||
(layer) => layer.id === props.parentLayerId
|
||||
);
|
||||
return parentLayer?.children?.length > 1;
|
||||
}
|
||||
// 否则直接返回根图层的可删除状态
|
||||
return props.layers.length > 3;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layers-list">
|
||||
<!-- 可排序的根级图层 -->
|
||||
<VueDraggable
|
||||
:model-value="sortableRootLayers"
|
||||
@end="handleRootLayersSort"
|
||||
class="sortable-layers"
|
||||
:animation="200"
|
||||
:disabled="false"
|
||||
handle=".layer-drag-handle"
|
||||
ghost-class="ghost"
|
||||
chosen-class="chosen"
|
||||
drag-class="drag"
|
||||
:group="groupName"
|
||||
>
|
||||
<!-- 遍历可排序的根级图层 -->
|
||||
<template v-for="(layer, index) in sortableRootLayers" :key="layer.id">
|
||||
<div class="layer-group">
|
||||
<!-- 使用 LayerItem 子组件 -->
|
||||
<LayerItem
|
||||
:layer="layer"
|
||||
:is-child="isChild"
|
||||
:is-active="layer.id === activeLayerId"
|
||||
:is-selected="isLayerSelected(layer.id)"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:is-editing="editingLayerId === layer.id"
|
||||
:editing-name="editingLayerName"
|
||||
:can-delete="
|
||||
canDeleteComputed &&
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
!layer.locked
|
||||
"
|
||||
:thumbnail-url="getLayerThumbnail(layer.id)"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
@click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@double-click="
|
||||
(...args) => forwardEvent('layer-double-click', ...args)
|
||||
"
|
||||
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
|
||||
@checkbox-change="
|
||||
(...args) => forwardEvent('checkbox-change', ...args)
|
||||
"
|
||||
@toggle-visibility="
|
||||
(...args) => forwardEvent('toggle-visibility', ...args)
|
||||
"
|
||||
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
|
||||
@delete="(...args) => forwardEvent('delete', ...args)"
|
||||
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
|
||||
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
|
||||
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
|
||||
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
|
||||
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
|
||||
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
|
||||
@update:editing-name="
|
||||
(...args) => forwardEvent('update:editing-name', ...args)
|
||||
"
|
||||
@toggle-group-expanded="
|
||||
(...args) => forwardEvent('toggle-group-expanded', ...args)
|
||||
"
|
||||
@toggle-child-visibility="
|
||||
(...args) => forwardEvent('toggle-child-visibility', ...args)
|
||||
"
|
||||
@toggle-child-lock="
|
||||
(...args) => forwardEvent('toggle-child-lock', ...args)
|
||||
"
|
||||
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
|
||||
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
|
||||
/>
|
||||
|
||||
<!-- 子图层列表 (递归渲染) -->
|
||||
<div
|
||||
v-if="
|
||||
layer?.children?.length > 0 &&
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
expandedGroupIds?.has(layer.id)
|
||||
"
|
||||
class="child-layers"
|
||||
>
|
||||
<LayersList
|
||||
:layers="layers"
|
||||
:sortableRootLayers="layer.children"
|
||||
:active-layer-id="activeLayerId"
|
||||
:selected-layer-ids="selectedLayerIds"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:editing-layer-id="editingLayerId"
|
||||
:editing-layer-name="editingLayerName"
|
||||
:thumbnail-manager="thumbnailManager"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
:isChild="true"
|
||||
:parentLayerId="layer.id"
|
||||
group-name="layers-child"
|
||||
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@layer-double-click="
|
||||
(...args) => forwardEvent('layer-double-click', ...args)
|
||||
"
|
||||
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
|
||||
@checkbox-change="
|
||||
(...args) => forwardEvent('checkbox-change', ...args)
|
||||
"
|
||||
@toggle-visibility="
|
||||
(...args) => forwardEvent('toggle-visibility', ...args)
|
||||
"
|
||||
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
|
||||
@delete="(...args) => forwardEvent('delete', ...args)"
|
||||
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
|
||||
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
|
||||
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
|
||||
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
|
||||
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
|
||||
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
|
||||
@update:editing-name="
|
||||
(...args) => forwardEvent('update:editing-name', ...args)
|
||||
"
|
||||
@root-layers-sort="
|
||||
(...args) => forwardEvent('root-layers-sort', ...args)
|
||||
"
|
||||
@child-layers-sort="
|
||||
(...args) => forwardEvent('child-layers-sort', ...args)
|
||||
"
|
||||
@select-child-layer="
|
||||
(...args) => forwardEvent('select-child-layer', ...args)
|
||||
"
|
||||
@start-child-layer-edit="
|
||||
(...args) => forwardEvent('start-child-layer-edit', ...args)
|
||||
"
|
||||
@child-context-menu="
|
||||
(...args) => forwardEvent('child-context-menu', ...args)
|
||||
"
|
||||
@toggle-child-visibility="
|
||||
(...args) => forwardEvent('toggle-child-visibility', ...args)
|
||||
"
|
||||
@toggle-child-lock="
|
||||
(...args) => forwardEvent('toggle-child-lock', ...args)
|
||||
"
|
||||
@finish-child-layer-edit="
|
||||
(...args) => forwardEvent('finish-child-layer-edit', ...args)
|
||||
"
|
||||
@cancel-child-layer-edit="
|
||||
(...args) => forwardEvent('cancel-child-layer-edit', ...args)
|
||||
"
|
||||
@child-layer-edit-keydown="
|
||||
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
|
||||
"
|
||||
@toggle-group-expanded="
|
||||
(...args) => forwardEvent('toggle-group-expanded', ...args)
|
||||
"
|
||||
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
|
||||
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 从父组件的样式文件中继承相关样式
|
||||
.layers-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.sortable-layers {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
// .layer-group {
|
||||
// // margin-bottom: 1px;
|
||||
// }
|
||||
|
||||
.child-layers {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
background-color: #e0e0e0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-layers {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
// 拖拽状态样式
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.layers-list {
|
||||
.child-layers {
|
||||
padding-left: 25px;
|
||||
&::after {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
// 右键菜单样式
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 1000;
|
||||
max-width: 280px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
// overflow: hidden;
|
||||
top: 60px; // 默认位置,可根据实际需要调整
|
||||
// 动画相关
|
||||
&.context-menu-enter-active,
|
||||
&.context-menu-leave-active {
|
||||
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.context-menu-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&.context-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
white-space: nowrap;
|
||||
min-height: 32px;
|
||||
position: relative;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
|
||||
.context-menu-icon {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: #fff2f0;
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-children {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-menu-label {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
margin-left: 16px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-arrow {
|
||||
margin-left: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
// 子菜单样式
|
||||
.context-submenu {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 1001;
|
||||
min-width: 160px;
|
||||
max-width: 280px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
// overflow: hidden;
|
||||
|
||||
&.submenu-left {
|
||||
left: auto;
|
||||
right: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单动画
|
||||
.context-submenu-enter-active,
|
||||
.context-submenu-leave-active {
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.context-submenu-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-8px);
|
||||
}
|
||||
|
||||
.context-submenu-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-4px);
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 768px) {
|
||||
.context-menu {
|
||||
min-width: 140px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
min-height: 36px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.context-submenu {
|
||||
min-width: 140px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题支持
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.context-menu {
|
||||
background-color: #1f1f1f;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.32),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-arrow {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.context-submenu {
|
||||
background-color: #1f1f1f;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.32),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
// 文本省略样式
|
||||
.text-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 主容器样式
|
||||
.layers-panel-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
z-index: 6;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.layers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-actions-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.normal-actions,
|
||||
.multi-select-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮样式
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: none;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
border-color: #e6e6e6;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
border-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊按钮样式
|
||||
.group-btn {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #91d5ff;
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
background-color: #bae7ff;
|
||||
border-color: #69c0ff;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
background-color: #f0f5ff;
|
||||
border-color: #d9ecff;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.ungroup-btn {
|
||||
background-color: #fff2e8;
|
||||
border-color: #ffbb96;
|
||||
color: #fa8c16;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffd8bf;
|
||||
border-color: #ff9c6e;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
background-color: #f0f5ff;
|
||||
border-color: #d9ecff;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-selected-btn {
|
||||
background-color: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffebe6;
|
||||
border-color: #ff7875;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-selection-btn {
|
||||
background-color: #f6f6f6;
|
||||
border-color: #d9d9d9;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
border-color: #bfbfbf;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
// 多选信息提示
|
||||
.multi-select-info {
|
||||
padding: 10px 6px;
|
||||
// background-color: #e6f7ff;
|
||||
background-color: rgba(238, 238, 238,0.4);
|
||||
border-bottom: 1px solid #91d5ff;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层列表
|
||||
.layers-list {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// 图层项样式
|
||||
.layer-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid #f5f2f2;
|
||||
padding-left: 30px;
|
||||
padding-right: 10px;
|
||||
|
||||
&.group-layer {
|
||||
background-color: rgba(240, 248, 255, 0.3);
|
||||
border-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #91d5ff;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #bae7ff;
|
||||
border-color: #91d5ff;
|
||||
// box-shadow: 0 0 0 1px #1890ff;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
background-color: #fff7e6;
|
||||
border-color: #ffd666;
|
||||
}
|
||||
|
||||
// &.multi-select-mode {
|
||||
// // padding-left: 30px;
|
||||
// }
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图层头部
|
||||
.layer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// 图层预览
|
||||
.layer-review {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
|
||||
10px 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.layer-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 可见性按钮
|
||||
.visibility-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.hidden {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层名称
|
||||
.layer-name-container {
|
||||
flex: 1;
|
||||
margin: 0 6px;
|
||||
overflow: hidden;
|
||||
// max-width: 204px;
|
||||
.layer-name-wrapper{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layer-name-input {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #1890ff;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 图层状态
|
||||
.layer-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 12px;
|
||||
|
||||
&.locked {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.group {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层操作
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
// pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽手柄
|
||||
.layer-drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
cursor: move;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
margin-right: 4px;
|
||||
background: #eee;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 复选框
|
||||
.layer-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 0;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// input[type="checkbox"] {
|
||||
// width: 16px;
|
||||
// height: 16px;
|
||||
// cursor: pointer;
|
||||
// accent-color: #1890ff;
|
||||
// }
|
||||
}
|
||||
|
||||
// 子图层样式
|
||||
.child-layers {
|
||||
}
|
||||
|
||||
.child-layer {
|
||||
padding: 8px 20px 8px 32px;
|
||||
background-color: rgba(240, 240, 240, 0.3);
|
||||
border-left: 2px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(224, 224, 224, 0.5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(230, 247, 255, 0.5);
|
||||
border-left: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
background-color: rgba(255, 247, 230, 0.5);
|
||||
border-left: 2px solid #ffd666;
|
||||
}
|
||||
|
||||
.layer-actions {
|
||||
position: static;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layer-indent {
|
||||
width: 20px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.layer-info {
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
|
||||
.layer-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.layer-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.layer-type {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.child-drag-handle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: move;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 固定图层样式
|
||||
.fixed-layers {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
// background-color: #fafafa;
|
||||
|
||||
background-color: rgba(238, 238, 238,0.4);
|
||||
.layer-drag-handle{
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-layer {
|
||||
background-color: #fafafa;
|
||||
// border-left: 3px solid #1890ff;
|
||||
|
||||
// &:hover {
|
||||
// background-color: #e6f7ff;
|
||||
// }
|
||||
}
|
||||
|
||||
.fixed-layer-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.background-indicator,
|
||||
.fixed-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.background-icon,
|
||||
.fixed-icon {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 拖拽样式
|
||||
.sortable-layers {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background-color: #f0f0f0;
|
||||
border: 2px dashed #1890ff;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
// 子图层拖拽样式
|
||||
.child-layers {
|
||||
.ghost {
|
||||
opacity: 0.4;
|
||||
background-color: #fff7e6;
|
||||
border: 2px dashed #faad14;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
background-color: #fff7e6;
|
||||
border: 1px solid #faad14;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.7;
|
||||
transform: rotate(3deg);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.layers-panel-inner {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
padding: 12px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.layer-drag-handle,
|
||||
.visibility-btn {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.layer-review {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.multi-select-info {
|
||||
// padding: 12px;
|
||||
|
||||
small {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
// .layer-name-container {
|
||||
// // max-width: 182px;
|
||||
// }
|
||||
}
|
||||
|
||||
// iPad 优化
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.layer-item {
|
||||
padding: 10px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.layer-drag-handle:hover,
|
||||
.visibility-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸设备优化
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.layer-item {
|
||||
padding-left: 30px;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: #d9d9d9;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组图层展开/收起图标样式
|
||||
.group-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 4px;
|
||||
|
||||
// &:hover {
|
||||
// background-color: rgba(0, 0, 0, 0.1);
|
||||
// }
|
||||
|
||||
// 展开/收起图标的过渡动画
|
||||
.svg-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 组图层样式
|
||||
.group-layer {
|
||||
.layer-type-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层缩进和连接线
|
||||
.child-layers {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-indent {
|
||||
width: 16px;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层展开/收起动画样式
|
||||
.child-layers-expand-enter-active,
|
||||
.child-layers-expand-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.child-layers-expand-enter-from {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.child-layers-expand-leave-to {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// 展开图标旋转动画优化
|
||||
.group-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
// 展开/收起图标的过渡动画
|
||||
.svg-icon {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层展开时的额外样式
|
||||
.child-layers {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(240, 248, 255, 0.1) 0%, rgba(240, 248, 255, 0.05) 100%);
|
||||
// border-radius: 4px;
|
||||
// margin-top: 2px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: linear-gradient(to bottom, #e0e0e0 0%, rgba(224, 224, 224, 0.3) 100%);
|
||||
}
|
||||
|
||||
// 子图层项动画
|
||||
.layer-item {
|
||||
animation: slideInRight 0.2s ease-out;
|
||||
animation-fill-mode: both;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0.05s; }
|
||||
&:nth-child(2) { animation-delay: 0.1s; }
|
||||
&:nth-child(3) { animation-delay: 0.15s; }
|
||||
&:nth-child(4) { animation-delay: 0.2s; }
|
||||
&:nth-child(5) { animation-delay: 0.25s; }
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层项进入动画
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端动画优化
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.child-layers-expand-enter-active,
|
||||
.child-layers-expand-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.group-expand-icon {
|
||||
&:hover {
|
||||
transform: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.child-layers .layer-item {
|
||||
animation-duration: 0.15s;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
@@ -156,7 +155,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.RED_BRUSH,
|
||||
title: "Red Brush (R)",
|
||||
action: () => selectTool(OperationType.RED_BRUSH),
|
||||
action: () => selectTool(OperationType.RED_BRUSH, true),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "red-brush-btn",
|
||||
style: { color: "#FF0000" },
|
||||
@@ -164,7 +163,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.GREEN_BRUSH,
|
||||
title: "Green Brush (G)",
|
||||
action: () => selectTool(OperationType.GREEN_BRUSH),
|
||||
action: () => selectTool(OperationType.GREEN_BRUSH, true),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "green-brush-btn",
|
||||
style: { color: "#00AA00" },
|
||||
@@ -172,7 +171,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.ERASER,
|
||||
title: "Eraser (E)",
|
||||
action: () => selectTool(OperationType.ERASER),
|
||||
action: () => selectTool(OperationType.ERASER, true),
|
||||
icon: { name: "CEraser", size: "22" },
|
||||
class: "eraser-btn",
|
||||
},
|
||||
@@ -197,8 +196,8 @@ const toolsList = computed(() => {
|
||||
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
||||
});
|
||||
|
||||
function selectTool(tool) {
|
||||
emit("tool-selected", tool);
|
||||
function selectTool(tool, isRedGreenMode = false) {
|
||||
emit("tool-selected", tool, isRedGreenMode);
|
||||
}
|
||||
|
||||
function triggerImageUpload() {
|
||||
@@ -261,15 +260,15 @@ function handleKeyDown(event) {
|
||||
|
||||
switch (key) {
|
||||
case "R":
|
||||
selectTool(OperationType.RED_BRUSH);
|
||||
selectTool(OperationType.RED_BRUSH, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "G":
|
||||
selectTool(OperationType.GREEN_BRUSH);
|
||||
selectTool(OperationType.GREEN_BRUSH, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "E":
|
||||
selectTool(OperationType.ERASER);
|
||||
selectTool(OperationType.ERASER, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
@@ -323,6 +322,8 @@ onUnmounted(() => {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
user-select: none;
|
||||
min-width: 58px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
defineAsyncComponent,
|
||||
shallowRef,
|
||||
provide,
|
||||
defineExpose,
|
||||
} from "vue";
|
||||
import { CanvasManager } from "./managers/CanvasManager";
|
||||
import { LayerManager } from "./managers/LayerManager";
|
||||
@@ -21,14 +22,14 @@ import { RedGreenModeManager } from "./managers/RedGreenModeManager";
|
||||
// 导入封装的组件
|
||||
import ToolsSidebar from "./components/ToolsSidebar.vue";
|
||||
import HeaderMenu from "./components/HeaderMenu.vue";
|
||||
import LayersPanel from "./components/LayersPanel.vue";
|
||||
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
||||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||
import { OperationType } from "./utils/layerHelper.js";
|
||||
import { ToolManager } from "./managers/ToolManager.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
|
||||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||||
const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
||||
@@ -70,13 +71,17 @@ const currentZoom = ref(100);
|
||||
const canvasWidth = ref(CanvasConfig.width);
|
||||
const canvasHeight = ref(CanvasConfig.height);
|
||||
const canvasColor = ref(CanvasConfig.backgroundColor);
|
||||
const layerWidth = ref(CanvasConfig.layerWidth); // 假设侧边栏宽度为 250px
|
||||
const layerWidth = ref(CanvasConfig.layerWidth);
|
||||
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
|
||||
const canvasManagerLoaded = ref(false); // 画布是否加载完成
|
||||
|
||||
// 红绿图模式状态
|
||||
const isRedGreenMode = ref(false);
|
||||
|
||||
const isShowLayerPanel = ref(true); // 是否显示图层面板
|
||||
|
||||
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
|
||||
|
||||
// 小地图设置
|
||||
// const minimapEnabled = ref(true);
|
||||
// const minimapManager = ref(null);
|
||||
@@ -110,7 +115,9 @@ function toggleShortcutHelp() {
|
||||
function handleToolSelect(tool) {
|
||||
activeTool.value = tool;
|
||||
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
|
||||
toolManager.setToolWithCommand(tool); // 命令模式 可撤回操作
|
||||
toolManager.setToolWithCommand(tool, {
|
||||
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
|
||||
}); // 命令模式 可撤回操作
|
||||
}
|
||||
|
||||
function toggleMinimap(enabled) {
|
||||
@@ -127,9 +134,10 @@ onMounted(async () => {
|
||||
canvasHeight.value = canvasContainerRef.value.clientWidth;
|
||||
canvasWidth.value = canvasContainerRef.value.clientHeight;
|
||||
}
|
||||
|
||||
// 创建管理器实例
|
||||
canvasManager = new CanvasManager(canvasRef.value, {
|
||||
width: canvasContainerRef.value.clientWidth - layerWidth.value, // 初始化的时候需要减去侧边栏宽度
|
||||
width: canvasContainerRef.value.clientWidth,
|
||||
height: canvasContainerRef.value.clientHeight,
|
||||
// backgroundColor: canvasColor.value,
|
||||
currentZoom,
|
||||
@@ -139,7 +147,6 @@ onMounted(async () => {
|
||||
canvasColor,
|
||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||
});
|
||||
console.log(canvasManager,canvasManager.thumbnailManager)
|
||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||
canvasManager.canvas.activeElementId = activeElementId;
|
||||
|
||||
@@ -155,6 +162,7 @@ onMounted(async () => {
|
||||
canvasWidth: canvasWidth.value,
|
||||
canvasHeight: canvasHeight.value,
|
||||
backgroundColor: canvasColor.value,
|
||||
isRedGreenMode: props.enabledRedGreenMode,
|
||||
layers,
|
||||
activeLayerId,
|
||||
canvasManager, // 添加对 canvasManager 的引用
|
||||
@@ -202,6 +210,7 @@ onMounted(async () => {
|
||||
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
|
||||
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
|
||||
canvasManager.setLayerManager(layerManager);
|
||||
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
|
||||
|
||||
// 初始化快捷键管理器
|
||||
keyboardManager = new KeyboardManager({
|
||||
@@ -369,13 +378,18 @@ function updateCanvasSize() {
|
||||
const containerWidth = canvasContainerRef.value.clientWidth;
|
||||
const containerHeight = canvasContainerRef.value.clientHeight;
|
||||
|
||||
// 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
||||
if (props.enabledRedGreenMode && layerManager) {
|
||||
layerManager.resizeCanvasWithScale(containerWidth, containerHeight);
|
||||
} else {
|
||||
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
}
|
||||
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
|
||||
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
||||
// if (props.enabledRedGreenMode && layerManager) {
|
||||
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
|
||||
// undoable: false, // 可撤销操作
|
||||
// });
|
||||
// } else {
|
||||
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
// canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,15 +402,17 @@ function addLayer() {
|
||||
}
|
||||
|
||||
function setActiveLayer(layerId) {
|
||||
if (activeElementId.value && canvasManager && canvasManager.canvas) {
|
||||
if (layerId !== activeLayerId.value) {
|
||||
layerManager.setActiveLayer(layerId, {
|
||||
undoable: true, // 可撤销
|
||||
});
|
||||
|
||||
const activeObject = canvasManager.canvas.getActiveObject();
|
||||
if (activeObject) {
|
||||
canvasManager.canvas.discardActiveObject();
|
||||
canvasManager.canvas.renderAll();
|
||||
}
|
||||
activeElementId.value = null;
|
||||
}
|
||||
layerManager.setActiveLayer(layerId);
|
||||
}
|
||||
|
||||
function toggleLayerVisibility(layerId) {
|
||||
@@ -570,9 +586,9 @@ defineExpose({
|
||||
getCanvasManager: () => canvasManager, // 获取画布管理器实例
|
||||
canvasManagerLoaded,
|
||||
// 加载新数据到画布
|
||||
loadJSON: (json) => {
|
||||
loadJSON: (json, calllBack) => {
|
||||
try {
|
||||
if (json) canvasManager?.loadJSON?.(json);
|
||||
if (json) canvasManager?.loadJSON?.(json, calllBack);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("加载画布JSON失败:", error);
|
||||
@@ -611,42 +627,133 @@ defineExpose({
|
||||
expPicType,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 移动图层位置
|
||||
* @param {string} layerId 图层ID
|
||||
* @param {string} direction 移动方向,'up'或'down'
|
||||
* @returns {boolean} 是否移动成功
|
||||
*/
|
||||
moveLayer(layerId, direction) {
|
||||
if (!layerManager) return false;
|
||||
const result = layerManager.moveLayer(layerId, direction);
|
||||
|
||||
// 使用高级排序重建画布顺序
|
||||
if (result) {
|
||||
layerManager.forceRebuildCanvasOrder();
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
reorderLayers(oldIndex, newIndex, layerId) {
|
||||
if (!layerManager) return false;
|
||||
|
||||
// 优先使用高级排序功能
|
||||
if (layerManager.layerSort) {
|
||||
return layerManager.advancedReorderLayers(oldIndex, newIndex, layerId);
|
||||
} else {
|
||||
// 降级到基础排序
|
||||
return layerManager.reorderLayers(oldIndex, newIndex, layerId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 智能排序图层
|
||||
* 根据对象类型和位置自动调整图层顺序
|
||||
* @param {Array<string>} targetLayerIds 要排序的图层ID数组,null表示排序所有普通图层
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
smartSortLayers(targetLayerIds = null) {
|
||||
if (!layerManager) return false;
|
||||
return layerManager.smartSortLayers(targetLayerIds);
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化图层结构
|
||||
* 清理空图层、重新排序等
|
||||
* @returns {Object} 优化结果统计
|
||||
*/
|
||||
optimizeLayerStructure() {
|
||||
if (!layerManager)
|
||||
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
|
||||
return layerManager.optimizeLayerStructure();
|
||||
},
|
||||
|
||||
/**
|
||||
* 强制重建画布对象顺序
|
||||
* 当图层顺序发生变化后调用此方法确保画布对象顺序正确
|
||||
*/
|
||||
forceRebuildCanvasOrder() {
|
||||
if (!layerManager) return;
|
||||
layerManager.forceRebuildCanvasOrder();
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证画布对象顺序是否正确
|
||||
* @returns {boolean} 顺序是否正确
|
||||
*/
|
||||
validateObjectOrder() {
|
||||
if (!layerManager) return true;
|
||||
return layerManager.validateObjectOrder();
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量重新排序多个图层
|
||||
* @param {Array} reorderOperations 排序操作数组 [{layerId, oldIndex, newIndex}]
|
||||
* @returns {boolean} 是否全部操作成功
|
||||
*/
|
||||
batchReorderLayers(reorderOperations) {
|
||||
if (!layerManager) return false;
|
||||
return layerManager.batchReorderLayers(reorderOperations);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 头部菜单组件 -->
|
||||
<HeaderMenu
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:canvasWidth="canvasWidth"
|
||||
:canvasHeight="canvasHeight"
|
||||
:canvasColor="canvasColor"
|
||||
:brushSize="brushSize"
|
||||
@update:canvasWidth="canvasWidth = $event"
|
||||
@update:canvasHeight="canvasHeight = $event"
|
||||
@update:canvasColor="canvasColor = $event"
|
||||
@update:brushSize="brushSize = $event"
|
||||
@canvas-size-change="updateCanvasSize"
|
||||
@canvas-color-change="updateCanvasColor"
|
||||
/>
|
||||
<div class="header-menu">
|
||||
<HeaderMenu
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:canvasWidth="canvasWidth"
|
||||
:canvasHeight="canvasHeight"
|
||||
:canvasColor="canvasColor"
|
||||
:brushSize="brushSize"
|
||||
:enabledRedGreenMode="enabledRedGreenMode"
|
||||
@update:canvasWidth="canvasWidth = $event"
|
||||
@update:canvasHeight="canvasHeight = $event"
|
||||
@update:canvasColor="canvasColor = $event"
|
||||
@update:brushSize="brushSize = $event"
|
||||
@canvas-size-change="updateCanvasSize"
|
||||
@canvas-color-change="updateCanvasColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- :minimapEnabled="minimapEnabled" -->
|
||||
<!-- 工具栏组件 -->
|
||||
<ToolsSidebar
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:isRedGreenMode="isRedGreenMode"
|
||||
@tool-selected="handleToolSelect"
|
||||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||||
@toggle-red-green-mode="toggleRedGreenMode"
|
||||
@trigger-image-upload="triggerImageUpload"
|
||||
@add-text="handleAddText"
|
||||
@zoom-in="zoomIn"
|
||||
@zoom-out="zoomOut"
|
||||
/>
|
||||
<div style="min-width: 58px">
|
||||
<ToolsSidebar
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:isRedGreenMode="isRedGreenMode"
|
||||
@tool-selected="handleToolSelect"
|
||||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||||
@toggle-red-green-mode="toggleRedGreenMode"
|
||||
@trigger-image-upload="triggerImageUpload"
|
||||
@add-text="handleAddText"
|
||||
@zoom-in="zoomIn"
|
||||
@zoom-out="zoomOut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="canvas-container"
|
||||
@@ -665,14 +772,14 @@ defineExpose({
|
||||
|
||||
<!-- 文本编辑面板 -->
|
||||
<TextEditorPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
/>
|
||||
|
||||
<!-- 液化编辑面板 -->
|
||||
<LiquifyPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
:liquifyManager="liquifyManager"
|
||||
@@ -682,7 +789,7 @@ defineExpose({
|
||||
|
||||
<!-- 选区面板 -->
|
||||
<SelectionPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
:selectionManager="selectionManager"
|
||||
@@ -705,22 +812,29 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<!-- 图层面板组件 -->
|
||||
<LayersPanel
|
||||
class="layers-panel"
|
||||
:style="{ width: layerWidth + 'px' }"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:activeLayerId="activeLayerId"
|
||||
:activeElementId="activeElementId"
|
||||
:thumbnailManager="canvasManager?.thumbnailManager"
|
||||
@add-layer="addLayer"
|
||||
@set-active-layer="setActiveLayer"
|
||||
@toggle-layer-visibility="toggleLayerVisibility"
|
||||
@move-layer-up="moveLayerUp"
|
||||
@move-layer-down="moveLayerDown"
|
||||
@remove-layer="removeLayer"
|
||||
@layers-reorder="handleLayersReorder"
|
||||
@child-layers-reorder="handleChildLayersReorder"
|
||||
/>
|
||||
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="layers-panel"
|
||||
v-if="isShowLayerPanel && !enabledRedGreenMode"
|
||||
>
|
||||
<LayersPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeLayerId="activeLayerId"
|
||||
:activeElementId="activeElementId"
|
||||
:thumbnailManager="canvasManager.thumbnailManager"
|
||||
@add-layer="addLayer"
|
||||
@set-active-layer="setActiveLayer"
|
||||
@toggle-layer-visibility="toggleLayerVisibility"
|
||||
@move-layer-up="moveLayerUp"
|
||||
@move-layer-down="moveLayerDown"
|
||||
@remove-layer="removeLayer"
|
||||
@layers-reorder="handleLayersReorder"
|
||||
@child-layers-reorder="handleChildLayersReorder"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="footer-actions">
|
||||
@@ -769,6 +883,15 @@ defineExpose({
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 77;
|
||||
& > .header-menu {
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@@ -972,10 +1095,33 @@ button:hover {
|
||||
}
|
||||
|
||||
.layers-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 10px;
|
||||
transition: width 0.3s ease;
|
||||
background: #fff;
|
||||
width: 250px;
|
||||
flex: none;
|
||||
width: 350px;
|
||||
max-height: 85vh;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
z-index: 1000; /* 确保面板在最上层 */
|
||||
border: 1px solid #e0e0e0;
|
||||
/* 添加指向整个面板的倒三角 */
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: 6px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 10px solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
|
||||
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
/* 添加触控设备的样式调整 */
|
||||
@media (pointer: coarse) {
|
||||
@@ -1016,4 +1162,15 @@ button:hover {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import initAligningGuidelines, {
|
||||
initCenteringGuidelines,
|
||||
} from "../utils/helperLine";
|
||||
@@ -14,10 +14,15 @@ import { createCanvas } from "../utils/canvasFactory";
|
||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
import { RedGreenModeManager } from "./RedGreenModeManager";
|
||||
import { EraserStateManager } from "./EraserStateManager";
|
||||
import { deepClone, optimizeCanvasRendering } from "../utils/helper";
|
||||
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
||||
import { isFunction } from "lodash-es";
|
||||
import {
|
||||
ChangeFixedImageCommand,
|
||||
AddImageToLayerCommand,
|
||||
} from "../commands/ObjectLayerCommands";
|
||||
restoreObjectLayerAssociations,
|
||||
simplifyLayers,
|
||||
validateLayerAssociations,
|
||||
} from "../utils/layerUtils";
|
||||
|
||||
export class CanvasManager {
|
||||
constructor(canvasElement, options) {
|
||||
@@ -34,6 +39,7 @@ export class CanvasManager {
|
||||
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
|
||||
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
|
||||
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
|
||||
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
||||
// 初始化画布
|
||||
this.initializeCanvas();
|
||||
}
|
||||
@@ -65,14 +71,6 @@ export class CanvasManager {
|
||||
layers: this.layers,
|
||||
});
|
||||
|
||||
// 初始化红绿图模式管理器
|
||||
this.redGreenModeManager = new RedGreenModeManager({
|
||||
canvas: this.canvas,
|
||||
layerManager: null, // 稍后设置
|
||||
toolManager: null, // 稍后设置
|
||||
commandManager: null, // 稍后设置
|
||||
});
|
||||
|
||||
// 设置画布辅助线
|
||||
initAligningGuidelines(this.canvas);
|
||||
|
||||
@@ -90,7 +88,7 @@ export class CanvasManager {
|
||||
*/
|
||||
_initCanvasEvents() {
|
||||
// 添加笔刷图像转换处理回调
|
||||
this.canvas.onBrushImageConverted = (fabricImage) => {
|
||||
this.canvas.onBrushImageConverted = async (fabricImage) => {
|
||||
// 如果图层管理器存在,将图像合并到当前活动图层
|
||||
if (this.layerManager) {
|
||||
// 获取当前活动图层
|
||||
@@ -107,21 +105,49 @@ export class CanvasManager {
|
||||
});
|
||||
|
||||
// 执行高保真合并操作
|
||||
this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
await this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
fabricImage,
|
||||
activeLayer,
|
||||
});
|
||||
|
||||
// 返回false表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return false;
|
||||
// 返回true表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return true;
|
||||
} else {
|
||||
console.warn("没有活动图层,使用默认行为添加图像");
|
||||
}
|
||||
}
|
||||
|
||||
// 返回true表示使用默认行为(直接添加到画布)
|
||||
return true;
|
||||
// 返回false表示使用默认行为(直接添加到画布)
|
||||
return false;
|
||||
};
|
||||
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", () => {
|
||||
console.log("开始擦除");
|
||||
this.eraserStateManager.startErasing();
|
||||
});
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", async (e) => {
|
||||
console.log("擦除完成", e.targets);
|
||||
// 可以在这里保存状态到命令管理器
|
||||
const affectedObjects = e.targets || [];
|
||||
const command = this.eraserStateManager.endErasing(affectedObjects);
|
||||
if (command && this.commandManager) {
|
||||
await this.commandManager?.executeCommand?.(command);
|
||||
} else {
|
||||
await command?.execute?.(); // 如果没有命令管理器,直接执行命令
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
command &&
|
||||
(await this.layerManager?.updateLayersObjectsInteractivity?.());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +194,10 @@ export class CanvasManager {
|
||||
if (this.redGreenModeManager) {
|
||||
this.redGreenModeManager.layerManager = this.layerManager;
|
||||
}
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.setLayerManager(this.layerManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +350,8 @@ export class CanvasManager {
|
||||
|
||||
/**
|
||||
* 居中所有画布元素
|
||||
* 计算所有对象的边界框,然后将它们整体居中显示
|
||||
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
||||
* 这样可以保持对象间的相对位置关系不变
|
||||
*/
|
||||
centerAllObjects() {
|
||||
if (!this.canvas) return;
|
||||
@@ -333,45 +364,56 @@ export class CanvasManager {
|
||||
(obj) => obj.visible !== false && !obj.excludeFromExport
|
||||
);
|
||||
|
||||
// 如果只有背景层或没有可见对象,只居中背景层
|
||||
if (
|
||||
visibleObjects.length === 0 ||
|
||||
(visibleObjects.length === 1 && visibleObjects[0].isBackground)
|
||||
) {
|
||||
// 尝试居中背景层
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
return;
|
||||
}
|
||||
// 如果没有可见对象,直接返回
|
||||
if (visibleObjects.length === 0) return;
|
||||
|
||||
// 单独处理背景层
|
||||
// 获取背景对象
|
||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||
const contentObjects = backgroundObject
|
||||
? visibleObjects.filter((obj) => obj !== backgroundObject)
|
||||
: visibleObjects;
|
||||
|
||||
// 如果只有背景层,居中背景层
|
||||
if (contentObjects.length === 0 && backgroundObject) {
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
// 如果只有背景层或没有背景层,使用原有逻辑
|
||||
if (!backgroundObject) {
|
||||
console.warn("未找到背景层,使用默认居中逻辑");
|
||||
// 如果只有一个对象且可能是背景,直接居中
|
||||
if (visibleObjects.length === 1) {
|
||||
const obj = visibleObjects[0];
|
||||
obj.set({
|
||||
left: this.width / 2,
|
||||
top: this.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
obj.setCoords();
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算内容对象的边界
|
||||
const bounds = this._calculateObjectsBounds(contentObjects);
|
||||
|
||||
// 计算所有对象的中心点
|
||||
const objectsCenterX = bounds.left + bounds.width / 2;
|
||||
const objectsCenterY = bounds.top + bounds.height / 2;
|
||||
// 记录背景层居中前的位置
|
||||
const backgroundOldLeft = backgroundObject.left;
|
||||
const backgroundOldTop = backgroundObject.top;
|
||||
|
||||
// 计算画布中心点
|
||||
const canvasCenterX = this.width / 2;
|
||||
const canvasCenterY = this.height / 2;
|
||||
|
||||
// 计算需要移动的距离
|
||||
const deltaX = canvasCenterX - objectsCenterX;
|
||||
const deltaY = canvasCenterY - objectsCenterY;
|
||||
// 设置背景层居中
|
||||
backgroundObject.set({
|
||||
left: canvasCenterX,
|
||||
top: canvasCenterY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
// 移动所有对象,包括背景层
|
||||
visibleObjects.forEach((obj) => {
|
||||
// 计算背景层的偏移量
|
||||
const deltaX = backgroundObject.left - backgroundOldLeft;
|
||||
const deltaY = backgroundObject.top - backgroundOldTop;
|
||||
|
||||
// 将相同的偏移量应用到所有其他对象上
|
||||
const otherObjects = visibleObjects.filter(
|
||||
(obj) => obj !== backgroundObject
|
||||
);
|
||||
|
||||
otherObjects.forEach((obj) => {
|
||||
obj.set({
|
||||
left: obj.left + deltaX,
|
||||
top: obj.top + deltaY,
|
||||
@@ -456,6 +498,7 @@ export class CanvasManager {
|
||||
// 如果需要裁剪背景层以外的内容,则更新蒙层位置
|
||||
// 创建或更新蒙层
|
||||
CanvasConfig.isCropBackground &&
|
||||
!this.enabledRedGreenMode &&
|
||||
this.createOrUpdateMask(backgroundLayerObject);
|
||||
return true;
|
||||
}
|
||||
@@ -480,12 +523,13 @@ export class CanvasManager {
|
||||
|
||||
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
||||
this.maskLayer = new fabric.Rect({
|
||||
id: "canvasMaskLayer",
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: left,
|
||||
top: top,
|
||||
fill: "transparent",
|
||||
stroke: "#cccccc",
|
||||
stroke: "transparent",
|
||||
strokeWidth: 1,
|
||||
strokeDashArray: [5, 5],
|
||||
selectable: false,
|
||||
@@ -504,20 +548,12 @@ export class CanvasManager {
|
||||
this.canvas.clipPath = new fabric.Rect({
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: 0,
|
||||
top: 0,
|
||||
left: left,
|
||||
top: top,
|
||||
originX: backgroundLayerObject.originX || "left",
|
||||
originY: backgroundLayerObject.originY || "top",
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
// 设置蒙层位置
|
||||
this.canvas.clipPath.set({
|
||||
left: left,
|
||||
top: top,
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
getBackgroundLayer() {
|
||||
if (!this.canvas) return null;
|
||||
@@ -655,6 +691,39 @@ export class CanvasManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 更改固定图层的图片
|
||||
* @param {String} imageUrl 新的图片URL
|
||||
* @param {Object} options 选项
|
||||
* @param {String} options.targetLayerType 目标图层类型(background/fixed)
|
||||
* @param {Boolean} options.undoable 是否可撤销,默认不可撤销
|
||||
* @return {Object} 执行结果
|
||||
* */
|
||||
async changeFixedImage(imageUrl, options = {}) {
|
||||
if (!this.layerManager) {
|
||||
console.error("图层管理器未设置,无法更改固定图层图片");
|
||||
return;
|
||||
}
|
||||
const command = new ChangeFixedImageCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
imageUrl: imageUrl,
|
||||
targetLayerType: options.targetLayerType || "fixed", // background/fixed
|
||||
});
|
||||
|
||||
command.undoable =
|
||||
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
|
||||
|
||||
return (
|
||||
(await command?.execute?.()) || {
|
||||
success: false,
|
||||
layerId: null,
|
||||
imageUrl: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图片
|
||||
* @param {Object} options 导出选项
|
||||
@@ -663,6 +732,7 @@ export class CanvasManager {
|
||||
* @param {String} options.layerId 导出具体图层ID
|
||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {String} 导出的图片数据URL
|
||||
*/
|
||||
exportImage(options = {}) {
|
||||
@@ -672,7 +742,39 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
try {
|
||||
return this.exportManager.exportImage(options);
|
||||
// 自动设置红绿图模式相关参数
|
||||
const enhancedOptions = {
|
||||
...options,
|
||||
// 如果没有明确指定,则根据当前模式自动设置
|
||||
restoreOpacityInRedGreen:
|
||||
options.restoreOpacityInRedGreen !== undefined
|
||||
? options.restoreOpacityInRedGreen
|
||||
: true, // 默认在红绿图模式下恢复透明度
|
||||
};
|
||||
|
||||
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
||||
if (
|
||||
this.enabledRedGreenMode &&
|
||||
!options.layerId &&
|
||||
(!options.layerIdArray || options.layerIdArray.length === 0)
|
||||
) {
|
||||
console.log("检测到红绿图模式,自动包含所有普通图层进行导出");
|
||||
|
||||
// 获取所有非背景、非固定的普通图层ID
|
||||
const normalLayerIds =
|
||||
this.layers?.value
|
||||
?.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
|
||||
)
|
||||
?.map((layer) => layer.id) || [];
|
||||
|
||||
if (normalLayerIds.length > 0) {
|
||||
enhancedOptions.layerIdArray = normalLayerIds;
|
||||
console.log("红绿图模式导出图层:", normalLayerIds);
|
||||
}
|
||||
}
|
||||
|
||||
return this.exportManager.exportImage(enhancedOptions);
|
||||
} catch (error) {
|
||||
console.error("CanvasManager导出图片失败:", error);
|
||||
throw error;
|
||||
@@ -709,129 +811,129 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
// 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
// let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 创建对象ID映射表,用于快速查找
|
||||
// tempLayers = tempLayers.map((layer) => {
|
||||
// const newLayer = { ...layer };
|
||||
|
||||
// 为所有fabric对象生成ID(如果没有的话)
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (!obj.id) {
|
||||
obj.id = `obj_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
});
|
||||
// // 处理fabricObjects数组
|
||||
// if (Array.isArray(layer.fabricObjects)) {
|
||||
// newLayer.fabricObjects = layer.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj, obj.id);
|
||||
}
|
||||
});
|
||||
// // 确保对象有ID
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
tempLayers = tempLayers.map((layer) => {
|
||||
const newLayer = { ...layer };
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object", // 保存类型信息用于调试
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newLayer.fabricObjects = [];
|
||||
// }
|
||||
|
||||
// 处理fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
newLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// if (layer.clippingMask) {
|
||||
// layer.clippingMask = {
|
||||
// id: layer.clippingMask.id,
|
||||
// };
|
||||
// }
|
||||
|
||||
// 确保对象有ID
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理单个fabricObject
|
||||
// if (layer.fabricObject) {
|
||||
// if (!layer.fabricObject.id) {
|
||||
// layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newLayer.fabricObject = {
|
||||
// id: layer.fabricObject.id,
|
||||
// type: layer.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newLayer.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object", // 保存类型信息用于调试
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newLayer.fabricObjects = [];
|
||||
}
|
||||
// // 处理子图层
|
||||
// if (Array.isArray(layer.children)) {
|
||||
// newLayer.children = layer.children.map((cItem) => {
|
||||
// const newChild = { ...cItem };
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
if (!layer.fabricObject.id) {
|
||||
layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newLayer.fabricObject = {
|
||||
id: layer.fabricObject.id,
|
||||
type: layer.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newLayer.fabricObject = null;
|
||||
}
|
||||
// // 处理子图层的fabricObjects
|
||||
// if (Array.isArray(cItem.fabricObjects)) {
|
||||
// newChild.fabricObjects = cItem.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 处理子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
newLayer.children = layer.children.map((cItem) => {
|
||||
const newChild = { ...cItem };
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
// 处理子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
newChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object",
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newChild.fabricObjects = [];
|
||||
// }
|
||||
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理子图层的fabricObject
|
||||
// if (cItem.fabricObject) {
|
||||
// if (!cItem.fabricObject.id) {
|
||||
// cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newChild.fabricObject = {
|
||||
// id: cItem.fabricObject.id,
|
||||
// type: cItem.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newChild.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object",
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newChild.fabricObjects = [];
|
||||
}
|
||||
|
||||
// 处理子图层的fabricObject
|
||||
if (cItem.fabricObject) {
|
||||
if (!cItem.fabricObject.id) {
|
||||
cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newChild.fabricObject = {
|
||||
id: cItem.fabricObject.id,
|
||||
type: cItem.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return newChild;
|
||||
});
|
||||
} else {
|
||||
newLayer.children = [];
|
||||
}
|
||||
|
||||
return newLayer;
|
||||
});
|
||||
// return newChild;
|
||||
// });
|
||||
// } else {
|
||||
// newLayer.children = [];
|
||||
// }
|
||||
|
||||
// return newLayer;
|
||||
// });
|
||||
try {
|
||||
console.log(
|
||||
"获取画布JSON数据...",
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
);
|
||||
return JSON.stringify({
|
||||
canvas: this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"excludeFromExport",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]),
|
||||
layers: tempLayers,
|
||||
layers: JSON.stringify(
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
), // 简化图层数据
|
||||
version: "1.0", // 添加版本信息
|
||||
timestamp: new Date().toISOString(), // 添加时间戳
|
||||
canvasWidth: this.canvasWidth.value,
|
||||
@@ -845,7 +947,7 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadJSON(json) {
|
||||
loadJSON(json, calllBack) {
|
||||
console.log("加载画布JSON数据:", json);
|
||||
|
||||
// 确保传入的json是字符串格式
|
||||
@@ -859,7 +961,7 @@ export class CanvasManager {
|
||||
const parsedJson = JSON.parse(json);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempLayers = parsedJson?.layers || [];
|
||||
const tempLayers = JSON.parse(parsedJson?.layers) || [];
|
||||
const canvasData = parsedJson?.canvas;
|
||||
|
||||
if (!tempLayers) {
|
||||
@@ -872,9 +974,11 @@ export class CanvasManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
this.layers.value = tempLayers;
|
||||
|
||||
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
// this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
|
||||
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
|
||||
|
||||
@@ -885,107 +989,57 @@ export class CanvasManager {
|
||||
this.canvas.clear();
|
||||
|
||||
// 加载画布数据
|
||||
this.canvas.loadFromJSON(canvasData, () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
this.canvas.loadFromJSON(canvasData, async () => {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
// 重新构建对象关系
|
||||
restoreObjectLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj.id, obj);
|
||||
}
|
||||
});
|
||||
// 验证图层关联关系 - 稳定后可以注释
|
||||
const isValidate = validateLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
// 辅助函数:根据ID查找对象
|
||||
const findObjectById = (id) => {
|
||||
if (!id) return null;
|
||||
return objectIdMap.get(id) || null;
|
||||
};
|
||||
console.log("图层关联验证结果:", isValidate);
|
||||
|
||||
// 恢复图层数据
|
||||
this.layers.value = tempLayers.map((layer) => {
|
||||
const restoredLayer = { ...layer };
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 恢复fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
restoredLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredLayer.fabricObjects = [];
|
||||
}
|
||||
// // 如果检测到红绿图模式内容,进行缩放调整
|
||||
// if (this.enabledRedGreenMode) {
|
||||
// this._rescaleRedGreenModeContent();
|
||||
// }
|
||||
|
||||
// 恢复单个fabricObject
|
||||
if (layer.fabricObject && layer.fabricObject.id) {
|
||||
restoredLayer.fabricObject = findObjectById(
|
||||
layer.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredLayer.fabricObject = null;
|
||||
}
|
||||
// 重载代码后支持回调中操作一些内容
|
||||
await calllBack?.();
|
||||
|
||||
// 恢复子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
restoredLayer.children = layer.children.map((cItem) => {
|
||||
const restoredChild = { ...cItem };
|
||||
// 确保所有对象的交互性正确设置
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(
|
||||
false
|
||||
);
|
||||
console.log(this.layerManager.layers.value);
|
||||
debugger;
|
||||
|
||||
// 恢复子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
restoredChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredChild.fabricObjects = [];
|
||||
}
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
// 恢复子图层的fabricObject
|
||||
if (cItem.fabricObject && cItem.fabricObject.id) {
|
||||
restoredChild.fabricObject = findObjectById(
|
||||
cItem.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return restoredChild;
|
||||
});
|
||||
} else {
|
||||
restoredLayer.children = [];
|
||||
}
|
||||
|
||||
return restoredLayer;
|
||||
});
|
||||
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 如果检测到红绿图模式内容,进行缩放调整
|
||||
if (this.enabledRedGreenMode) {
|
||||
this._rescaleRedGreenModeContent();
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { EraserCommand } from "../commands/EraserCommand";
|
||||
|
||||
/**
|
||||
* 橡皮擦状态管理器
|
||||
* 用于管理橡皮擦操作的状态快照
|
||||
*/
|
||||
export class EraserStateManager {
|
||||
constructor(canvas, layerManager) {
|
||||
this.canvas = canvas;
|
||||
this.layerManager = layerManager;
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
|
||||
setLayerManager(layerManager) {
|
||||
this.layerManager = layerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始橡皮擦操作 - 捕获初始状态
|
||||
*/
|
||||
startErasing() {
|
||||
console.log("橡皮擦操作开始 - 捕获状态快照");
|
||||
this.currentSnapshot = this._captureCanvasSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束橡皮擦操作 - 创建命令
|
||||
* @param {Array} affectedObjects 受影响的对象
|
||||
* @returns {EraserCommand|null} 创建的橡皮擦命令
|
||||
*/
|
||||
endErasing(affectedObjects = []) {
|
||||
if (!this.currentSnapshot) {
|
||||
console.warn("没有初始状态快照,无法创建橡皮擦命令");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!affectedObjects || affectedObjects.length === 0) {
|
||||
console.log("没有对象被擦除,不创建命令");
|
||||
this.currentSnapshot = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`橡皮擦操作结束 - 影响了 ${affectedObjects.length} 个对象`);
|
||||
|
||||
// 捕获擦除后的状态
|
||||
const afterSnapshot = this._captureCanvasSnapshot();
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const command = new EraserCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
affectedObjects: affectedObjects,
|
||||
beforeSnapshot: this.currentSnapshot,
|
||||
afterSnapshot: afterSnapshot,
|
||||
});
|
||||
|
||||
// 重置状态
|
||||
this.currentSnapshot = null;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获画布状态快照
|
||||
* @returns {Object} 画布状态快照
|
||||
* @private
|
||||
*/
|
||||
_captureCanvasSnapshot() {
|
||||
try {
|
||||
return this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("捕获画布状态快照失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* 液化对象引用管理器
|
||||
* 专门处理液化操作中的对象引用管理,避免引用丢失问题
|
||||
*/
|
||||
export class LiquifyReferenceManager {
|
||||
constructor() {
|
||||
// 对象引用池
|
||||
this.objectRefs = new Map();
|
||||
// 状态快照池
|
||||
this.stateSnapshots = new Map();
|
||||
// ImageData缓存池
|
||||
this.imageDataCache = new Map();
|
||||
// 事件监听器备份
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册对象到引用管理器
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {String} objectId 对象唯一ID
|
||||
* @returns {String} 引用ID
|
||||
*/
|
||||
registerObject(fabricObject, objectId) {
|
||||
const refId = objectId || this._generateRefId();
|
||||
|
||||
// 保存对象引用
|
||||
this.objectRefs.set(refId, fabricObject);
|
||||
|
||||
// 备份事件监听器
|
||||
this._backupEventListeners(refId, fabricObject);
|
||||
|
||||
// 创建初始状态快照
|
||||
this._createStateSnapshot(refId, fabricObject);
|
||||
|
||||
console.log(`📝 对象已注册到引用管理器: ${refId}`);
|
||||
return refId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象引用
|
||||
* @param {String} refId 引用ID
|
||||
* @returns {Object|null} Fabric对象
|
||||
*/
|
||||
getObjectRef(refId) {
|
||||
return this.objectRefs.get(refId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对象的图像数据,保持引用不变
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} newImageData 新的图像数据
|
||||
* @returns {Promise<Boolean>} 更新结果
|
||||
*/
|
||||
async updateObjectImageData(refId, newImageData) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject || !newImageData) {
|
||||
throw new Error(`无法更新对象图像数据: 对象不存在或数据无效 (${refId})`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = newImageData.width;
|
||||
tempCanvas.height = newImageData.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.putImageData(newImageData, 0, 0);
|
||||
|
||||
// 创建新的图像元素
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 保存当前状态
|
||||
const currentState = this._captureObjectState(fabricObject);
|
||||
|
||||
// 更新图像源
|
||||
if (fabricObject.setElement) {
|
||||
fabricObject.setElement(img);
|
||||
} else if (fabricObject._element) {
|
||||
fabricObject._element = img;
|
||||
fabricObject._originalElement = img;
|
||||
}
|
||||
|
||||
// 恢复非图像属性
|
||||
this._restoreObjectState(fabricObject, currentState);
|
||||
|
||||
// 标记需要重新渲染
|
||||
fabricObject.dirty = true;
|
||||
fabricObject.setCoords();
|
||||
|
||||
// 更新缓存
|
||||
this._updateImageDataCache(refId, newImageData);
|
||||
|
||||
console.log(`✅ 对象图像数据更新成功: ${refId}`);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
console.error("更新对象状态失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("加载图像数据失败"));
|
||||
};
|
||||
|
||||
img.src = tempCanvas.toDataURL();
|
||||
} catch (error) {
|
||||
console.error("创建临时canvas失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对象状态快照
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {String} 快照ID
|
||||
*/
|
||||
createSnapshot(refId, snapshotId = null) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject) {
|
||||
throw new Error(`无法创建快照: 对象不存在 (${refId})`);
|
||||
}
|
||||
|
||||
const snapId = snapshotId || `${refId}_${Date.now()}`;
|
||||
const snapshot = this._createStateSnapshot(snapId, fabricObject);
|
||||
|
||||
console.log(`📸 已创建对象快照: ${snapId}`);
|
||||
return snapId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象到指定快照状态
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {Promise<Boolean>} 恢复结果
|
||||
*/
|
||||
async restoreFromSnapshot(refId, snapshotId) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
const snapshot = this.stateSnapshots.get(snapshotId);
|
||||
|
||||
if (!fabricObject || !snapshot) {
|
||||
throw new Error(
|
||||
`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`
|
||||
);
|
||||
}
|
||||
|
||||
// 恢复图像数据
|
||||
if (snapshot.imageData) {
|
||||
await this.updateObjectImageData(refId, snapshot.imageData);
|
||||
}
|
||||
|
||||
// 恢复对象属性
|
||||
if (snapshot.properties) {
|
||||
this._restoreObjectState(fabricObject, snapshot.properties);
|
||||
}
|
||||
|
||||
// 恢复事件监听器
|
||||
if (snapshot.eventListeners) {
|
||||
this._restoreEventListeners(refId, fabricObject, snapshot.eventListeners);
|
||||
}
|
||||
|
||||
console.log(`🔄 对象快照恢复成功: ${refId} -> ${snapshotId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新多个对象
|
||||
* @param {Array} updates 更新列表 [{refId, imageData}, ...]
|
||||
* @returns {Promise<Array>} 更新结果
|
||||
*/
|
||||
async batchUpdate(updates) {
|
||||
const results = [];
|
||||
|
||||
for (const update of updates) {
|
||||
try {
|
||||
const result = await this.updateObjectImageData(
|
||||
update.refId,
|
||||
update.imageData
|
||||
);
|
||||
results.push({ refId: update.refId, success: true, result });
|
||||
} catch (error) {
|
||||
console.error(`批量更新失败 ${update.refId}:`, error);
|
||||
results.push({
|
||||
refId: update.refId,
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理不再使用的引用
|
||||
* @param {String} refId 引用ID
|
||||
*/
|
||||
cleanup(refId) {
|
||||
this.objectRefs.delete(refId);
|
||||
this.stateSnapshots.delete(refId);
|
||||
this.imageDataCache.delete(refId);
|
||||
this.eventListeners.delete(refId);
|
||||
|
||||
console.log(`🗑️ 已清理对象引用: ${refId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有引用
|
||||
*/
|
||||
cleanupAll() {
|
||||
this.objectRefs.clear();
|
||||
this.stateSnapshots.clear();
|
||||
this.imageDataCache.clear();
|
||||
this.eventListeners.clear();
|
||||
|
||||
console.log("🗑️ 已清理所有对象引用");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getMemoryStats() {
|
||||
return {
|
||||
objectRefs: this.objectRefs.size,
|
||||
stateSnapshots: this.stateSnapshots.size,
|
||||
imageDataCache: this.imageDataCache.size,
|
||||
eventListeners: this.eventListeners.size,
|
||||
totalMemoryUsage: this._calculateMemoryUsage(),
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 生成引用ID
|
||||
* @returns {String} 引用ID
|
||||
* @private
|
||||
*/
|
||||
_generateRefId() {
|
||||
return `ref_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @private
|
||||
*/
|
||||
_backupEventListeners(refId, fabricObject) {
|
||||
const listeners = {};
|
||||
|
||||
// 备份常见的事件监听器
|
||||
const eventTypes = [
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
];
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
if (
|
||||
fabricObject.__eventListeners &&
|
||||
fabricObject.__eventListeners[eventType]
|
||||
) {
|
||||
listeners[eventType] = [...fabricObject.__eventListeners[eventType]];
|
||||
}
|
||||
});
|
||||
|
||||
this.eventListeners.set(refId, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} listeners 监听器备份
|
||||
* @private
|
||||
*/
|
||||
_restoreEventListeners(refId, fabricObject, listeners) {
|
||||
Object.keys(listeners).forEach((eventType) => {
|
||||
if (listeners[eventType] && listeners[eventType].length > 0) {
|
||||
listeners[eventType].forEach((listener) => {
|
||||
fabricObject.on(eventType, listener);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建状态快照
|
||||
* @param {String} snapId 快照ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 快照数据
|
||||
* @private
|
||||
*/
|
||||
_createStateSnapshot(snapId, fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this._captureObjectState(fabricObject),
|
||||
imageData: this._captureImageData(fabricObject),
|
||||
eventListeners: this.eventListeners.get(snapId.split("_")[0]) || {},
|
||||
};
|
||||
|
||||
this.stateSnapshots.set(snapId, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 对象状态
|
||||
* @private
|
||||
*/
|
||||
_captureObjectState(fabricObject) {
|
||||
return {
|
||||
left: fabricObject.left,
|
||||
top: fabricObject.top,
|
||||
scaleX: fabricObject.scaleX,
|
||||
scaleY: fabricObject.scaleY,
|
||||
angle: fabricObject.angle,
|
||||
flipX: fabricObject.flipX,
|
||||
flipY: fabricObject.flipY,
|
||||
opacity: fabricObject.opacity,
|
||||
visible: fabricObject.visible,
|
||||
selectable: fabricObject.selectable,
|
||||
evented: fabricObject.evented,
|
||||
id: fabricObject.id,
|
||||
layerId: fabricObject.layerId,
|
||||
customData: fabricObject.customData ? { ...fabricObject.customData } : {},
|
||||
filters: fabricObject.filters ? [...fabricObject.filters] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} state 状态数据
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectState(fabricObject, state) {
|
||||
if (!state) return;
|
||||
|
||||
fabricObject.set({
|
||||
left: state.left,
|
||||
top: state.top,
|
||||
scaleX: state.scaleX,
|
||||
scaleY: state.scaleY,
|
||||
angle: state.angle,
|
||||
flipX: state.flipX,
|
||||
flipY: state.flipY,
|
||||
opacity: state.opacity,
|
||||
visible: state.visible,
|
||||
selectable: state.selectable,
|
||||
evented: state.evented,
|
||||
});
|
||||
|
||||
// 恢复自定义属性
|
||||
if (state.customData) {
|
||||
fabricObject.customData = { ...state.customData };
|
||||
}
|
||||
|
||||
// 恢复滤镜
|
||||
if (state.filters && state.filters.length > 0) {
|
||||
fabricObject.filters = [...state.filters];
|
||||
fabricObject.applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获图像数据
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {ImageData|null} 图像数据
|
||||
* @private
|
||||
*/
|
||||
_captureImageData(fabricObject) {
|
||||
try {
|
||||
if (
|
||||
fabricObject._element &&
|
||||
fabricObject._element.width &&
|
||||
fabricObject._element.height
|
||||
) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = fabricObject._element.width;
|
||||
canvas.height = fabricObject._element.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(fabricObject._element, 0, 0);
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("无法捕获图像数据:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图像数据缓存
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_updateImageDataCache(refId, imageData) {
|
||||
this.imageDataCache.set(refId, {
|
||||
data: imageData,
|
||||
timestamp: Date.now(),
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算内存使用量(近似值)
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* @private
|
||||
*/
|
||||
_calculateMemoryUsage() {
|
||||
let totalBytes = 0;
|
||||
|
||||
// 计算ImageData缓存大小
|
||||
this.imageDataCache.forEach((cache) => {
|
||||
totalBytes += cache.width * cache.height * 4; // RGBA = 4 bytes per pixel
|
||||
});
|
||||
|
||||
return totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单例引用管理器
|
||||
*/
|
||||
let liquifyReferenceManagerInstance = null;
|
||||
|
||||
export function getLiquifyReferenceManager() {
|
||||
if (!liquifyReferenceManagerInstance) {
|
||||
liquifyReferenceManagerInstance = new LiquifyReferenceManager();
|
||||
}
|
||||
return liquifyReferenceManagerInstance;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
|
||||
|
||||
@@ -35,7 +35,6 @@ export class RedGreenModeManager {
|
||||
* @param {String} options.redGreenImageUrl 红绿图URL
|
||||
* @param {Number} options.normalLayerOpacity 普通图层透明度 (0-1)
|
||||
* @param {Function} options.onImageGenerated 图片生成回调
|
||||
* @param {Boolean} options.useBatchMode 是否使用批量模式 (默认true,减少闪烁)
|
||||
* @returns {Promise<boolean>} 是否初始化成功
|
||||
*/
|
||||
async initialize(options = {}) {
|
||||
@@ -70,13 +69,8 @@ export class RedGreenModeManager {
|
||||
throw new Error("缺少必需的图片URL参数");
|
||||
}
|
||||
|
||||
// 使用批量模式或传统模式
|
||||
const useBatchMode = options.useBatchMode !== false; // 默认为true
|
||||
|
||||
let initCommand;
|
||||
|
||||
// 使用新的批量初始化命令,减少页面闪烁
|
||||
initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
const initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
toolManager: this.toolManager,
|
||||
@@ -94,10 +88,11 @@ export class RedGreenModeManager {
|
||||
await initCommand.execute();
|
||||
}
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
// 标记为已初始化
|
||||
this.isInitialized = true;
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
|
||||
// 启用图层管理器的红绿图模式
|
||||
if (
|
||||
this.layerManager &&
|
||||
@@ -106,6 +101,9 @@ export class RedGreenModeManager {
|
||||
this.layerManager.enableRedGreenMode();
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
|
||||
// 重置工具管理器状态
|
||||
// 默认红色笔刷
|
||||
if (this.toolManager) {
|
||||
@@ -116,7 +114,6 @@ export class RedGreenModeManager {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
批量模式: useBatchMode ? "已启用" : "已禁用",
|
||||
画布背景: "白色",
|
||||
});
|
||||
|
||||
@@ -131,17 +128,17 @@ export class RedGreenModeManager {
|
||||
// 注册鼠标抬起事件
|
||||
registerRedGreenMouseUpEvent() {
|
||||
this.canvas.on("mouse:up", (event) => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 可以在这里添加更多逻辑,比如生成图片或更新状态
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = this.canvasManager.exportImage();
|
||||
console.log("生成红绿图图片数据:", imageData);
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
requestAnimationFrame(async () => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = await this.canvasManager.exportImage();
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { BrushManager } from "./brushes/brushManager";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
InitLiquifyToolCommand,
|
||||
RasterizeForLiquifyCommand,
|
||||
} from "../commands/LiquifyCommands";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/GroupCommands";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
/**
|
||||
* 工具管理器
|
||||
@@ -378,6 +378,8 @@ export class ToolManager {
|
||||
previousTool: this.activeTool.value,
|
||||
});
|
||||
|
||||
command.undoable = options.undoable !== undefined ? options.undoable : true;
|
||||
|
||||
// 执行命令
|
||||
this.commandManager.execute(command, { ...options });
|
||||
}
|
||||
@@ -690,18 +692,9 @@ export class ToolManager {
|
||||
}
|
||||
} else if (checkResult.needsRasterization) {
|
||||
// 需要栅格化 (多个对象或组)
|
||||
// 询问用户是否要栅格化
|
||||
if (
|
||||
confirm(
|
||||
checkResult.isGroup
|
||||
? "组对象需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
: "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(activeLayerId);
|
||||
return; // 栅格化后会重新调用液化功能,这里直接返回
|
||||
}
|
||||
// 使用Modal询问用户是否要栅格化
|
||||
this._showRasterizeConfirmModal(checkResult.isGroup, activeLayerId);
|
||||
return; // 等待用户确认,不继续执行
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,51 +709,42 @@ export class ToolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并准备液化操作
|
||||
* 显示栅格化确认Modal对话框
|
||||
* @param {Boolean} isGroup 是否为组对象
|
||||
* @param {String} layerId 图层ID
|
||||
* @private
|
||||
*/
|
||||
_checkAndPrepareForLiquify(layerId) {
|
||||
// 确保存在液化管理器
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
return;
|
||||
}
|
||||
_showRasterizeConfirmModal(isGroup, layerId) {
|
||||
const title = "栅格化图层";
|
||||
const content = "需要先栅格化才能进行液化操作,是否立即栅格化?";
|
||||
|
||||
// 检查图层是否适合液化
|
||||
const checkResult = liquifyManager.checkLayerForLiquify(layerId);
|
||||
|
||||
if (checkResult.isEmpty) {
|
||||
// 空图层
|
||||
alert("当前图层为空,无法进行液化操作");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.isGroup) {
|
||||
// 询问是否栅格化组
|
||||
if (confirm("组对象需要栅格化才能进行液化操作,是否立即栅格化?")) {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText: "确定栅格化",
|
||||
cancelText: "取消",
|
||||
centered: true,
|
||||
icon: h("span", { style: "color: #faad14;" }, "⚠️"),
|
||||
onOk: () => {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.needsRasterization) {
|
||||
// 询问是否栅格化图层
|
||||
if (
|
||||
confirm(
|
||||
"当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果图层可以直接液化(单个图像对象)
|
||||
if (checkResult.valid) {
|
||||
this._startLiquify(layerId);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log("用户取消了栅格化操作");
|
||||
// 用户取消,触发液化面板显示事件但不能液化
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
detail: {
|
||||
activeLayerId: layerId,
|
||||
layerStatus: { needsRasterization: true, isGroup },
|
||||
canLiquify: false,
|
||||
targetObject: null,
|
||||
originalImageData: null,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -772,25 +756,52 @@ export class ToolManager {
|
||||
if (!this.commandManager || !this.layerManager) return;
|
||||
|
||||
try {
|
||||
// 导入液化相关命令
|
||||
// 显示加载Modal
|
||||
const loadingModal = Modal.info({
|
||||
title: "正在栅格化",
|
||||
content: "正在栅格化图层,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 创建栅格化命令
|
||||
const rasterizeCommand = new RasterizeForLiquifyCommand({
|
||||
const rasterizeCommand = new RasterizeLayerCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
layerId: layerId,
|
||||
layers: this.layerManager.layers,
|
||||
activeLayerId: this.layerManager.activeLayerId,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
const result = await this.commandManager.execute(rasterizeCommand);
|
||||
|
||||
// 关闭加载Modal
|
||||
loadingModal.destroy();
|
||||
|
||||
if (result) {
|
||||
// 栅格化成功,启动液化
|
||||
message.success("图层已成功栅格化,可以进行液化操作");
|
||||
this._startLiquify(layerId);
|
||||
} else {
|
||||
// 栅格化失败
|
||||
Modal.error({
|
||||
title: "栅格化失败",
|
||||
content: "栅格化失败,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("栅格化图层失败:", error);
|
||||
alert("栅格化失败,无法进行液化操作");
|
||||
Modal.error({
|
||||
title: "栅格化错误",
|
||||
content: `栅格化失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +814,12 @@ export class ToolManager {
|
||||
// 获取图层信息
|
||||
const layer = this.layerManager.getLayerById(layerId);
|
||||
if (!layer) {
|
||||
console.error("图层不存在");
|
||||
Modal.error({
|
||||
title: "图层错误",
|
||||
content: "图层不存在",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,14 +828,24 @@ export class ToolManager {
|
||||
if (layer.isBackground) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (!layer.fabricObject) {
|
||||
console.error("背景图层为空");
|
||||
Modal.warning({
|
||||
title: "背景图层为空",
|
||||
content: "背景图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObject;
|
||||
} else {
|
||||
// 普通图层使用 fabricObjects (复数)
|
||||
if (!layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.error("图层为空");
|
||||
Modal.warning({
|
||||
title: "图层为空",
|
||||
content: "图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObjects[0];
|
||||
@@ -828,11 +854,26 @@ export class ToolManager {
|
||||
// 确保liquifyManager可用
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
Modal.error({
|
||||
title: "液化管理器错误",
|
||||
content: "液化管理器未初始化",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示准备中的Modal
|
||||
const preparingModal = Modal.info({
|
||||
title: "准备液化环境",
|
||||
content: "正在准备液化环境,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 准备液化环境
|
||||
liquifyManager.initialize({
|
||||
canvas: this.canvas,
|
||||
@@ -855,6 +896,9 @@ export class ToolManager {
|
||||
// 执行初始化命令
|
||||
await this.commandManager.execute(initCommand);
|
||||
|
||||
// 关闭准备Modal
|
||||
preparingModal.destroy();
|
||||
|
||||
// 触发液化面板显示事件
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
@@ -867,7 +911,12 @@ export class ToolManager {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("启动液化工具失败:", error);
|
||||
alert("启动液化工具失败:" + error.message);
|
||||
Modal.error({
|
||||
title: "液化工具启动失败",
|
||||
content: `启动液化工具失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,49 @@ export class TexturePresetManager {
|
||||
cacheSize: this.textureCache.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证纹理文件
|
||||
* @param {File} file 要验证的文件
|
||||
* @returns {Boolean} 是否为有效的纹理文件
|
||||
*/
|
||||
validateTextureFile(file) {
|
||||
if (!file) {
|
||||
console.warn("文件不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.warn("文件类型无效,必须是图片文件");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为 10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
console.warn("文件大小超过限制(10MB)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查支持的图片格式
|
||||
const supportedTypes = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
if (!supportedTypes.includes(file.type)) {
|
||||
console.warn("不支持的图片格式");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import "./fabric.brushes.js";
|
||||
import { BrushStore } from "../../store/BrushStore";
|
||||
import { brushRegistry } from "./BrushRegistry";
|
||||
|
||||
@@ -16,7 +17,7 @@ import { MarkerBrush } from "./types/MarkerBrush";
|
||||
import { CustomPenBrush } from "./types/CustomPenBrush";
|
||||
import { RibbonBrush } from "./types/RibbonBrush";
|
||||
import { ShadedBrush } from "./types/ShadedBrush";
|
||||
import { SketchyBrush } from "./types/SketchyBrush";
|
||||
// import { SketchyBrush } from "./types/SketchyBrush";
|
||||
import { SpraypaintBrush } from "./types/SpraypaintBrush";
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,11 @@ export class BrushManager {
|
||||
|
||||
// 初始化笔刷注册
|
||||
this._registerDefaultBrushes();
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
this.eraserStateManager = null;
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = []; // 当前擦除会话中被影响的对象
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,64 +57,112 @@ export class BrushManager {
|
||||
_registerDefaultBrushes() {
|
||||
// 注册铅笔笔刷
|
||||
brushRegistry.register("pencil", PencilBrush, {
|
||||
name: "铅笔",
|
||||
name: "Pencil",
|
||||
description: "基础铅笔工具,适合精细线条绘制",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册材质笔刷
|
||||
brushRegistry.register("texture", TextureBrush);
|
||||
brushRegistry.register("texture", TextureBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册集成的笔刷类型
|
||||
brushRegistry.register("crayon", CrayonBrush);
|
||||
brushRegistry.register("fur", FurBrush);
|
||||
brushRegistry.register("ink", InkBrush);
|
||||
brushRegistry.register("longfur", LongfurBrush);
|
||||
brushRegistry.register("writing", WritingBrush);
|
||||
brushRegistry.register("marker", MarkerBrush);
|
||||
brushRegistry.register("pen", CustomPenBrush);
|
||||
brushRegistry.register("ribbon", RibbonBrush);
|
||||
brushRegistry.register("shaded", ShadedBrush);
|
||||
brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush);
|
||||
brushRegistry.register("crayon", CrayonBrush, {
|
||||
name: "Crayon",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("fur", FurBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ink", InkBrush, {
|
||||
name: "Ink",
|
||||
description: "墨水笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("", LongfurBrush, {
|
||||
name: "Longfur",
|
||||
description: "长毛发笔刷,适合绘制动物毛皮、草或头发",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("writing", WritingBrush, {
|
||||
name: "Writing",
|
||||
description: "书法笔刷,模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("marker", MarkerBrush, {
|
||||
name: "Marker",
|
||||
description: "马克笔笔刷,适合粗线条和填充",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("pen", CustomPenBrush, {
|
||||
name: "Pen",
|
||||
description: "自定义钢笔笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ribbon", RibbonBrush, {
|
||||
name: "Ribbon",
|
||||
description: "丝带笔刷,适合创建流动的丝带效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("shaded", ShadedBrush, {
|
||||
name: "Shaded",
|
||||
description: "阴影笔刷,适合创建渐变和阴影效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
// brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush, {
|
||||
name: "Spraypaint",
|
||||
description: "喷漆笔刷,模拟喷漆效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册喷枪笔刷
|
||||
brushRegistry.register(
|
||||
"spray",
|
||||
class SprayBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "spray",
|
||||
name: "喷枪",
|
||||
description: "模拟喷枪效果,创建散点效果",
|
||||
category: "基础笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册喷枪笔刷
|
||||
// brushRegistry.register(
|
||||
// "spray",
|
||||
// class SprayBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "spray",
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// category: "基础笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.SprayBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
// create() {
|
||||
// this.brush = new fabric.SprayBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
if (options.density !== undefined) {
|
||||
brush.density = options.density;
|
||||
}
|
||||
// if (options.density !== undefined) {
|
||||
// brush.density = options.density;
|
||||
// }
|
||||
|
||||
if (options.randomOpacity !== undefined) {
|
||||
brush.randomOpacity = options.randomOpacity;
|
||||
}
|
||||
// if (options.randomOpacity !== undefined) {
|
||||
// brush.randomOpacity = options.randomOpacity;
|
||||
// }
|
||||
|
||||
if (options.dotWidth !== undefined) {
|
||||
brush.dotWidth = options.dotWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
// if (options.dotWidth !== undefined) {
|
||||
// brush.dotWidth = options.dotWidth;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// }
|
||||
// );
|
||||
// 注册橡皮擦笔刷
|
||||
brushRegistry.register(
|
||||
"eraser",
|
||||
@@ -187,87 +241,87 @@ export class BrushManager {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册水彩笔刷
|
||||
brushRegistry.register(
|
||||
"watercolor",
|
||||
class WatercolorBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "watercolor",
|
||||
name: "水彩",
|
||||
description: "模拟水彩效果,带有流动感和透明感",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册水彩笔刷
|
||||
// brushRegistry.register(
|
||||
// "watercolor",
|
||||
// class WatercolorBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "watercolor",
|
||||
// name: "水彩",
|
||||
// description: "模拟水彩效果,带有流动感和透明感",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
// 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// // 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 水彩效果特有的属性
|
||||
this.brush.globalCompositeOperation = "multiply";
|
||||
this.brush.shadow = new fabric.Shadow({
|
||||
color: this.options.color || "#000",
|
||||
blur: 5,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
// // 水彩效果特有的属性
|
||||
// this.brush.globalCompositeOperation = "multiply";
|
||||
// this.brush.shadow = new fabric.Shadow({
|
||||
// color: this.options.color || "#000",
|
||||
// blur: 5,
|
||||
// offsetX: 0,
|
||||
// offsetY: 0,
|
||||
// });
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 水彩笔刷特有的配置
|
||||
brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 水彩笔刷特有的配置
|
||||
// brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// 注册粉笔笔刷
|
||||
brushRegistry.register(
|
||||
"chalk",
|
||||
class ChalkBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "chalk",
|
||||
name: "粉笔",
|
||||
description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册粉笔笔刷
|
||||
// brushRegistry.register(
|
||||
// "chalk",
|
||||
// class ChalkBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "chalk",
|
||||
// name: "粉笔",
|
||||
// description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 自定义绘画方法来模拟粉笔效果
|
||||
const originalOnMouseMove = this.brush.onMouseMove;
|
||||
this.brush.onMouseMove = function (pointer, options) {
|
||||
// 随机调整坐标位置,增加粉笔质感
|
||||
const jitter = 2;
|
||||
pointer.x += (Math.random() - 0.5) * jitter;
|
||||
pointer.y += (Math.random() - 0.5) * jitter;
|
||||
// // 自定义绘画方法来模拟粉笔效果
|
||||
// const originalOnMouseMove = this.brush.onMouseMove;
|
||||
// this.brush.onMouseMove = function (pointer, options) {
|
||||
// // 随机调整坐标位置,增加粉笔质感
|
||||
// const jitter = 2;
|
||||
// pointer.x += (Math.random() - 0.5) * jitter;
|
||||
// pointer.y += (Math.random() - 0.5) * jitter;
|
||||
|
||||
// 调用原始方法
|
||||
originalOnMouseMove.call(this, pointer, options);
|
||||
};
|
||||
// // 调用原始方法
|
||||
// originalOnMouseMove.call(this, pointer, options);
|
||||
// };
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 粉笔特有的设置
|
||||
brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 粉笔特有的设置
|
||||
// brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,6 +625,148 @@ export class BrushManager {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置依赖管理器
|
||||
*/
|
||||
setManagers({ layerManager, commandManager }) {
|
||||
this.layerManager = layerManager;
|
||||
this.commandManager = commandManager;
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
if (this.canvas && this.layerManager) {
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @param {Object} options 橡皮擦选项
|
||||
*/
|
||||
createEraser(options = {}) {
|
||||
if (!this.canvas) {
|
||||
console.error("画布未初始化");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
|
||||
this.brush = new fabric.EraserBrush(this.canvas);
|
||||
|
||||
// 应用配置
|
||||
this.configure(this.brush, {
|
||||
width: this.brushSize.value,
|
||||
color: this.brushColor.value,
|
||||
opacity: this.brushOpacity.value,
|
||||
inverted: options.inverted || false,
|
||||
...options,
|
||||
});
|
||||
|
||||
// 设置画布为绘图模式
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.freeDrawingBrush = this.brush;
|
||||
|
||||
// 绑定橡皮擦事件处理器
|
||||
// this._bindEraserEvents();
|
||||
|
||||
console.log("橡皮擦创建成功");
|
||||
return this.brush;
|
||||
} catch (error) {
|
||||
console.error("创建橡皮擦失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_bindEraserEvents() {
|
||||
if (!this.canvas || !this.eraserStateManager) return;
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", this._handleErasingStart.bind(this));
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_unbindEraserEvents() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.canvas.off("erasing:start", this._handleErasingStart.bind(this));
|
||||
this.canvas.off("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除开始事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingStart(e) {
|
||||
console.log("橡皮擦开始擦除");
|
||||
|
||||
if (!this.eraserStateManager) return;
|
||||
|
||||
// 标记擦除状态
|
||||
this.isErasingActive = true;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
// 捕获擦除前的状态
|
||||
this.eraserStateManager.startErasing();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除结束事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingEnd(e) {
|
||||
console.log("橡皮擦擦除结束", e);
|
||||
|
||||
if (!this.eraserStateManager || !this.isErasingActive) return;
|
||||
|
||||
// 获取被擦除的对象
|
||||
const affectedObjects = e.targets || [];
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const eraserCommand = this.eraserStateManager.endErasing(affectedObjects);
|
||||
|
||||
// 如果有有效的命令且有命令管理器,执行命令
|
||||
if (eraserCommand && this.commandManager) {
|
||||
// 注意:不需要调用 execute(),因为擦除操作已经完成
|
||||
// 只需要将命令添加到历史记录中以支持撤销
|
||||
this.commandManager.addToHistory(eraserCommand);
|
||||
console.log("橡皮擦操作已添加到命令历史");
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
if (!this.isErasingActive) return;
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.cancelErasing();
|
||||
}
|
||||
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
console.log("当前擦除操作已取消");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前笔刷类型
|
||||
* @returns {String} 当前笔刷类型ID
|
||||
@@ -646,7 +842,6 @@ export class BrushManager {
|
||||
|
||||
return this.canvas.freeDrawingBrush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @returns {Object} 橡皮擦笔刷
|
||||
@@ -703,9 +898,20 @@ export class BrushManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源
|
||||
* 销毁笔刷管理器
|
||||
*/
|
||||
dispose() {
|
||||
// 解绑事件
|
||||
// this._unbindEraserEvents();
|
||||
|
||||
// 取消进行中的擦除操作
|
||||
this.cancelErasing();
|
||||
|
||||
// 清理状态管理器
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager = null;
|
||||
}
|
||||
|
||||
// 销毁当前笔刷
|
||||
if (this.activeBrush) {
|
||||
this.activeBrush.destroy();
|
||||
|
||||
1748
src/component/Canvas/CanvasEditor/managers/brushes/fabric.brushes.js
Normal file
@@ -1,5 +1,5 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import texturePresetManager from "../TexturePresetManager";
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,72 @@ export class TextureBrush extends BaseBrush {
|
||||
// 创建fabric原生纹理笔刷
|
||||
this.brush = new fabric.PatternBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
|
||||
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
|
||||
this.brush
|
||||
);
|
||||
const self = this; // 保存外部this引用
|
||||
|
||||
this.brush._finalizeAndAddPath = function () {
|
||||
console.log("TextureBrush: _finalizeAndAddPath called");
|
||||
const ctx = this.canvas.contextTop;
|
||||
ctx.closePath();
|
||||
|
||||
// 应用点简化(如果PatternBrush支持)
|
||||
if (this.decimate && this._points) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"TextureBrush: points count =",
|
||||
this._points ? this._points.length : 0
|
||||
);
|
||||
|
||||
// 检查是否有有效的路径数据
|
||||
if (!this._points || this._points.length < 2) {
|
||||
// 如果点数不足,直接请求重新渲染
|
||||
console.log("TextureBrush: Not enough points, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const pathData = this.convertPointsToSVGPath(this._points);
|
||||
|
||||
const isEmpty = self._isEmptySVGPath(pathData);
|
||||
console.log("TextureBrush: isEmpty =", isEmpty);
|
||||
|
||||
if (isEmpty) {
|
||||
// 如果路径为空,直接请求重新渲染
|
||||
console.log("TextureBrush: Path is empty, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先触发事件,模拟原生行为
|
||||
const path = this.createPath(pathData);
|
||||
this.canvas.fire("before:path:created", { path: path });
|
||||
|
||||
console.log("TextureBrush: Calling convertToImg");
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
this.convertToImg();
|
||||
console.log("TextureBrush: convertToImg called successfully");
|
||||
} else {
|
||||
console.warn(
|
||||
"convertToImg method not found, falling back to original behavior"
|
||||
);
|
||||
// 如果没有convertToImg方法,回退到原始行为
|
||||
this.canvas.add(path);
|
||||
this.canvas.fire("path:created", { path: path });
|
||||
this.canvas.clearContext(this.canvas.contextTop);
|
||||
}
|
||||
|
||||
// 重置阴影
|
||||
this._resetShadow();
|
||||
};
|
||||
|
||||
// 配置笔刷基本属性
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 如果有选中的材质,则设置纹理
|
||||
@@ -221,6 +286,11 @@ export class TextureBrush extends BaseBrush {
|
||||
canvasTexture.width = width;
|
||||
canvasTexture.height = height;
|
||||
|
||||
// 应用透明度设置
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
}
|
||||
|
||||
// 绘制前应用旋转
|
||||
if (this.textureAngle !== 0) {
|
||||
ctx.save();
|
||||
@@ -233,26 +303,16 @@ export class TextureBrush extends BaseBrush {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
|
||||
// 应用透明度
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
// 缓存处理后的纹理图像
|
||||
this._processedTextureCanvas = canvasTexture;
|
||||
|
||||
// 创建Pattern对象
|
||||
const pattern = new fabric.Pattern({
|
||||
source: canvasTexture,
|
||||
repeat: this.textureRepeat,
|
||||
});
|
||||
// 使用fabric.js 5.x正确的API方式设置PatternBrush
|
||||
// 直接设置source属性,这是PatternBrush的标准用法
|
||||
this.brush.source = canvasTexture;
|
||||
|
||||
// 设置笔刷源纹理
|
||||
if (typeof this.brush.setSource === "function") {
|
||||
this.brush.setSource(pattern);
|
||||
} else if (typeof this.brush.source === "object") {
|
||||
this.brush.source = pattern;
|
||||
} else if (typeof this.brush.pattern === "object") {
|
||||
this.brush.pattern = pattern;
|
||||
// 通知画布重绘
|
||||
if (this.canvas && this.canvas.requestRenderAll) {
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,4 +912,37 @@ export class TextureBrush extends BaseBrush {
|
||||
getTextureStats() {
|
||||
return texturePresetManager.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SVG 路径是否为空
|
||||
* @private
|
||||
* @param {Array} pathData SVG 路径数据
|
||||
* @returns {Boolean} 是否为空路径
|
||||
*/
|
||||
_isEmptySVGPath(pathData) {
|
||||
if (!pathData || pathData.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路径是否只包含移动命令或者是一个点
|
||||
let hasDrawing = false;
|
||||
let moveCount = 0;
|
||||
|
||||
for (let i = 0; i < pathData.length; i++) {
|
||||
const command = pathData[i];
|
||||
if (command[0] === "M") {
|
||||
moveCount++;
|
||||
} else if (
|
||||
command[0] === "L" ||
|
||||
command[0] === "Q" ||
|
||||
command[0] === "C"
|
||||
) {
|
||||
hasDrawing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径
|
||||
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ export class CanvasEventManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并图层中的对象为图像以提高性能
|
||||
* 合并图层中的对象为组以提高性能
|
||||
* @param {Object} options 合并选项
|
||||
* @param {fabric.Image} options.fabricImage 新的图像对象
|
||||
* @param {Object} options.activeLayer 当前活动图层
|
||||
@@ -594,7 +594,6 @@ export class CanvasEventManager {
|
||||
console.warn("合并对象失败:没有活动图层");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否需要合并
|
||||
const hasExistingObjects =
|
||||
Array.isArray(activeLayer.fabricObjects) &&
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
import { LayerType } from "../../utils/layerHelper";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
@@ -313,16 +314,40 @@ export class EnhancedLiquifyManager {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.setParam === "function"
|
||||
) {
|
||||
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
|
||||
this.activeRenderer.setParam(param, value);
|
||||
} else {
|
||||
console.warn(
|
||||
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`EnhancedLiquifyManager: 无效参数 ${param}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("EnhancedLiquifyManager 批量设置参数:", params);
|
||||
|
||||
if (params && typeof params === "object") {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.setParam(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -348,6 +373,36 @@ export class EnhancedLiquifyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.startDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.startDeformation(x, y);
|
||||
}
|
||||
console.log(
|
||||
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.endDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.endDeformation();
|
||||
}
|
||||
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Object} target 目标对象
|
||||
@@ -468,7 +523,9 @@ export class EnhancedLiquifyManager {
|
||||
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
|
||||
}
|
||||
}
|
||||
|
||||
setRealtimeUpdater(realtimeUpdater) {
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
}
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
@@ -519,7 +576,7 @@ export class EnhancedLiquifyManager {
|
||||
|
||||
// 检查图层是否为空
|
||||
let objectsToCheck = [];
|
||||
if (layer.isBackground || layer.type === "background") {
|
||||
if (layer.isBackground || layer.type === "background" || layer.isFixed) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (layer.fabricObject) {
|
||||
objectsToCheck = [layer.fabricObject];
|
||||
@@ -548,7 +605,10 @@ export class EnhancedLiquifyManager {
|
||||
objectsToCheck[0].type === "rasterized-layer");
|
||||
|
||||
// 检查是否为组
|
||||
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
|
||||
const isGroup =
|
||||
objectsToCheck.some((obj) => obj.type === "group") ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
layer.children?.length > 0;
|
||||
|
||||
// 如果不是单一图像,需要栅格化
|
||||
const needsRasterization = !isImage || isGroup;
|
||||
@@ -572,26 +632,34 @@ export class EnhancedLiquifyManager {
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
// 创建临时canvas - 关键修复:使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
|
||||
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
|
||||
// 使用图像的原始尺寸,而不是缩放后的尺寸
|
||||
tempCanvas.width = fabricObject.width;
|
||||
tempCanvas.height = fabricObject.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
if (fabricObject._element) {
|
||||
// 绘制原始尺寸的图像
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
@@ -615,6 +683,13 @@ export class EnhancedLiquifyManager {
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 混合液化管理器 - 根据模式智能选择算法
|
||||
*/
|
||||
export class HybridLiquifyManager {
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 16,
|
||||
maxStrength: options.maxStrength || 200,
|
||||
smoothingIterations: options.smoothingIterations || 1,
|
||||
relaxFactor: options.relaxFactor || 0.1,
|
||||
};
|
||||
|
||||
this.params = {
|
||||
size: 80,
|
||||
pressure: 0.8,
|
||||
distortion: 0,
|
||||
power: 0.8,
|
||||
};
|
||||
|
||||
this.modes = {
|
||||
PUSH: "push",
|
||||
CLOCKWISE: "clockwise",
|
||||
COUNTERCLOCKWISE: "counterclockwise",
|
||||
PINCH: "pinch",
|
||||
EXPAND: "expand",
|
||||
CRYSTAL: "crystal",
|
||||
EDGE: "edge",
|
||||
RECONSTRUCT: "reconstruct",
|
||||
};
|
||||
|
||||
// 定义哪些模式使用像素算法
|
||||
this.pixelModes = new Set([
|
||||
this.modes.CLOCKWISE,
|
||||
this.modes.COUNTERCLOCKWISE,
|
||||
this.modes.CRYSTAL,
|
||||
this.modes.EDGE,
|
||||
]);
|
||||
|
||||
// 定义哪些模式使用网格算法
|
||||
this.meshModes = new Set([
|
||||
this.modes.PUSH,
|
||||
this.modes.PINCH,
|
||||
this.modes.EXPAND,
|
||||
this.modes.RECONSTRUCT,
|
||||
]);
|
||||
|
||||
this.currentMode = this.modes.PUSH;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
// 持续按压相关状态
|
||||
this.pressStartTime = 0;
|
||||
this.pressDuration = 0;
|
||||
this.accumulatedRotation = 0;
|
||||
this.accumulatedScale = 0;
|
||||
this.isHolding = false;
|
||||
this.continuousTimer = null;
|
||||
|
||||
// 鼠标状态
|
||||
this.initialMouseX = 0;
|
||||
this.initialMouseY = 0;
|
||||
this.currentMouseX = 0;
|
||||
this.currentMouseY = 0;
|
||||
this.isDragging = false;
|
||||
this.dragDistance = 0;
|
||||
}
|
||||
|
||||
// ...existing initialization methods...
|
||||
|
||||
/**
|
||||
* 应用液化变形 - 智能选择算法
|
||||
*/
|
||||
applyDeformation(x, y) {
|
||||
if (!this.initialized || !this.originalImageData) {
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
// 更新鼠标位置
|
||||
this.currentMouseX = x;
|
||||
this.currentMouseY = y;
|
||||
|
||||
const { size, pressure, power } = this.params;
|
||||
const radius = size;
|
||||
const strength = pressure * power;
|
||||
|
||||
// 根据模式选择算法
|
||||
if (this.pixelModes.has(this.currentMode)) {
|
||||
return this._applyPixelDeformation(x, y, radius, strength);
|
||||
} else if (this.meshModes.has(this.currentMode)) {
|
||||
return this._applyMeshDeformation(x, y, radius, strength);
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级液化算法 - 适用于旋转、水晶、边缘模式
|
||||
*/
|
||||
_applyPixelDeformation(centerX, centerY, radius, strength) {
|
||||
const data = this.currentImageData.data;
|
||||
const width = this.currentImageData.width;
|
||||
const height = this.currentImageData.height;
|
||||
const tempData = new Uint8ClampedArray(data);
|
||||
|
||||
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||
|
||||
// 计算影响区域边界
|
||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + processRadius));
|
||||
const minY = Math.max(0, Math.floor(centerY - processRadius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + processRadius));
|
||||
|
||||
switch (this.currentMode) {
|
||||
case this.modes.CLOCKWISE:
|
||||
case this.modes.COUNTERCLOCKWISE:
|
||||
this._applyPixelRotation(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength,
|
||||
this.currentMode === this.modes.CLOCKWISE
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.CRYSTAL:
|
||||
this._applyPixelCrystal(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.EDGE:
|
||||
this._applyPixelEdge(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级旋转算法
|
||||
*/
|
||||
_applyPixelRotation(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength,
|
||||
clockwise
|
||||
) {
|
||||
// 计算旋转角度
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
|
||||
const baseRotationSpeed = 0.015;
|
||||
const rotationAngle =
|
||||
(clockwise ? 1 : -1) *
|
||||
baseRotationSpeed *
|
||||
strength *
|
||||
(1.0 + timeFactor * 0.3);
|
||||
|
||||
this.accumulatedRotation += rotationAngle;
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
// 距离衰减:内圈快,外圈慢
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减
|
||||
|
||||
// 计算旋转后的源位置
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
||||
|
||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||
|
||||
// 双线性插值采样
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级水晶效果
|
||||
*/
|
||||
_applyPixelCrystal(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
||||
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const crystalRadius = normalizedDistance;
|
||||
|
||||
// 多层波浪扭曲
|
||||
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
|
||||
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
|
||||
const waveAngle =
|
||||
angle + (wave1 + wave2) * distortionStrength * falloff;
|
||||
|
||||
const radialMod =
|
||||
1 +
|
||||
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
|
||||
0.3;
|
||||
const modDistance = distance * radialMod;
|
||||
|
||||
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
|
||||
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
const factor = falloff * distortionStrength * 0.7;
|
||||
|
||||
// 混合原始颜色和扭曲颜色
|
||||
const originalIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = Math.round(
|
||||
srcData[originalIdx] * (1 - factor) + color[0] * factor
|
||||
);
|
||||
dstData[targetIdx + 1] = Math.round(
|
||||
srcData[originalIdx + 1] * (1 - factor) + color[1] * factor
|
||||
);
|
||||
dstData[targetIdx + 2] = Math.round(
|
||||
srcData[originalIdx + 2] * (1 - factor) + color[2] * factor
|
||||
);
|
||||
dstData[targetIdx + 3] = srcData[originalIdx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级边缘效果
|
||||
*/
|
||||
_applyPixelEdge(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
|
||||
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const edgeRadius = normalizedDistance;
|
||||
|
||||
const edgeWave =
|
||||
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
|
||||
Math.cos(angle * 6 + this.pressDuration * 0.002);
|
||||
const perpAngle = angle + Math.PI / 2;
|
||||
|
||||
const edgeFactor = edgeWave * falloff * edgeStrength * 0.5;
|
||||
const offsetX = Math.cos(perpAngle) * edgeFactor;
|
||||
const offsetY = Math.sin(perpAngle) * edgeFactor;
|
||||
|
||||
const sourceX = x + offsetX;
|
||||
const sourceY = y + offsetY;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双线性插值采样
|
||||
*/
|
||||
_bilinearSample(data, width, height, x, y) {
|
||||
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x1 = Math.floor(x);
|
||||
const y1 = Math.floor(y);
|
||||
const x2 = x1 + 1;
|
||||
const y2 = y1 + 1;
|
||||
|
||||
const fx = x - x1;
|
||||
const fy = y - y1;
|
||||
|
||||
const getPixel = (px, py) => {
|
||||
const idx = (py * width + px) * 4;
|
||||
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
|
||||
};
|
||||
|
||||
const p1 = getPixel(x1, y1);
|
||||
const p2 = getPixel(x2, y1);
|
||||
const p3 = getPixel(x1, y2);
|
||||
const p4 = getPixel(x2, y2);
|
||||
|
||||
return [
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[0] +
|
||||
fx * (1 - fy) * p2[0] +
|
||||
(1 - fx) * fy * p3[0] +
|
||||
fx * fy * p4[0]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[1] +
|
||||
fx * (1 - fy) * p2[1] +
|
||||
(1 - fx) * fy * p3[1] +
|
||||
fx * fy * p4[1]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[2] +
|
||||
fx * (1 - fy) * p2[2] +
|
||||
(1 - fx) * fy * p3[2] +
|
||||
fx * fy * p4[2]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[3] +
|
||||
fx * (1 - fy) * p2[3] +
|
||||
(1 - fx) * fy * p3[3] +
|
||||
fx * fy * p4[3]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格液化算法 - 适用于推拉、捏合、展开模式
|
||||
*/
|
||||
_applyMeshDeformation(x, y, radius, strength) {
|
||||
if (!this.mesh) return this.currentImageData;
|
||||
|
||||
// 使用现有的网格算法处理推拉、捏合、展开
|
||||
const mode = this.currentMode;
|
||||
const { distortion } = this.params;
|
||||
|
||||
this._applyDeformation(x, y, radius, strength, mode, distortion);
|
||||
|
||||
if (this.config.smoothingIterations > 0) {
|
||||
this._smoothMesh();
|
||||
}
|
||||
|
||||
return this._applyMeshToImage();
|
||||
}
|
||||
|
||||
// ...existing mesh methods...
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class LiquifyManager {
|
||||
meshResolution: options.meshResolution || 64,
|
||||
// 根据环境选择合适的渲染模式
|
||||
forceCPU: true, // 默认不强制使用CPU
|
||||
forceWebGL: false, // 优先使用WebGL模式
|
||||
forceWebGL: true, // 优先使用WebGL模式
|
||||
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
|
||||
layerManager: options.layerManager || null,
|
||||
canvas: options.canvas || null,
|
||||
@@ -87,9 +87,19 @@ export class LiquifyManager {
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
console.log(`LiquifyManager 设置参数: ${param}=${value}`);
|
||||
return this.enhancedManager.setParam(param, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("LiquifyManager 批量设置参数:", params);
|
||||
return this.enhancedManager.setParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -120,21 +130,14 @@ export class LiquifyManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保设置正确的模式和参数
|
||||
if (mode) {
|
||||
this.enhancedManager.setMode(mode);
|
||||
}
|
||||
console.log(
|
||||
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
|
||||
params
|
||||
);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.enhancedManager.setParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用液化变形
|
||||
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
|
||||
try {
|
||||
// 直接调用EnhancedLiquifyManager的applyLiquify方法
|
||||
// 避免重复设置参数,让EnhancedLiquifyManager处理参数设置
|
||||
const resultData = await this.enhancedManager.applyLiquify(
|
||||
targetObject,
|
||||
mode,
|
||||
@@ -146,6 +149,13 @@ export class LiquifyManager {
|
||||
// 确保返回结果数据
|
||||
if (!resultData) {
|
||||
console.warn("液化变形没有返回结果数据");
|
||||
} else {
|
||||
console.log(
|
||||
"✅ 液化变形成功,返回图像数据尺寸:",
|
||||
resultData.width,
|
||||
"x",
|
||||
resultData.height
|
||||
);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
@@ -180,6 +190,22 @@ export class LiquifyManager {
|
||||
return this.enhancedManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
return this.enhancedManager.startLiquifyOperation(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
return this.enhancedManager.endLiquifyOperation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 液化实时更新器
|
||||
* 负责高效地更新液化效果到画布上,避免频繁创建fabric对象导致的性能问题
|
||||
*/
|
||||
export class LiquifyRealTimeUpdater {
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.targetObject = null;
|
||||
this.isUpdating = false;
|
||||
this.updateQueue = [];
|
||||
this.lastUpdateTime = 0;
|
||||
this.currImage = options.currImage || { value: null };
|
||||
|
||||
// 配置选项
|
||||
this.config = {
|
||||
throttleTime: options.throttleTime || 16, // 60fps
|
||||
maxQueueSize: options.maxQueueSize || 5,
|
||||
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
|
||||
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
|
||||
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
|
||||
};
|
||||
|
||||
// 临时canvas用于快速渲染
|
||||
this.tempCanvas = document.createElement("canvas");
|
||||
this.tempCtx = this.tempCanvas.getContext("2d");
|
||||
|
||||
// 高质量canvas用于最终输出
|
||||
this.highQualityCanvas = document.createElement("canvas");
|
||||
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
|
||||
|
||||
// 当前缓存的图像数据
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.renderingScheduled = false;
|
||||
|
||||
// 优化Canvas画布渲染设置
|
||||
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||||
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标对象
|
||||
* @param {Object} fabricObject fabric图像对象
|
||||
*/
|
||||
setTargetObject(fabricObject) {
|
||||
this.targetObject = fabricObject;
|
||||
if (fabricObject && fabricObject._element) {
|
||||
// 设置临时canvas尺寸
|
||||
this.tempCanvas.width = fabricObject.width;
|
||||
this.tempCanvas.height = fabricObject.height;
|
||||
|
||||
// 设置高质量canvas尺寸
|
||||
this.highQualityCanvas.width = fabricObject.width;
|
||||
this.highQualityCanvas.height = fabricObject.height;
|
||||
|
||||
// 配置高质量渲染上下文
|
||||
this.highQualityCtx.imageSmoothingEnabled = true;
|
||||
this.highQualityCtx.imageSmoothingQuality = "high";
|
||||
|
||||
// 配置临时canvas上下文(快速渲染)
|
||||
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时更新图像数据到画布
|
||||
* @param {ImageData} imageData 新的图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
|
||||
* @returns {Promise} 更新完成的Promise
|
||||
*/
|
||||
async updateImage(imageData, isDrawing = false) {
|
||||
if (!this.targetObject || !imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流控制
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
|
||||
// 在绘制过程中进行节流,缓存最新的图像数据
|
||||
this.pendingImageData = imageData;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdateTime = now;
|
||||
|
||||
if (isDrawing && this.config.useDirectUpdate) {
|
||||
// 拖拽过程中使用快速更新(降低质量以提高性能)
|
||||
this._fastUpdate(imageData);
|
||||
} else {
|
||||
// 拖拽结束后使用完整更新(最高质量)
|
||||
await this._fullUpdate(imageData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能图像质量更新
|
||||
* 根据图像尺寸和设备性能动态调整质量
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制
|
||||
* @private
|
||||
*/
|
||||
_getOptimalQuality(imageData, isDrawing) {
|
||||
const pixelCount = imageData.width * imageData.height;
|
||||
|
||||
if (isDrawing) {
|
||||
// 拖拽时根据图像大小调整质量
|
||||
if (pixelCount > 1000000) {
|
||||
// 大于1M像素
|
||||
return 0.7;
|
||||
} else if (pixelCount > 500000) {
|
||||
// 大于500K像素
|
||||
return 0.8;
|
||||
} else {
|
||||
return 0.9;
|
||||
}
|
||||
} else {
|
||||
// 拖拽结束时始终使用最高质量
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速更新 - 直接修改现有对象的图像源
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_fastUpdate(imageData) {
|
||||
if (!this.targetObject || !this.targetObject._element) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将ImageData渲染到临时canvas(快速模式)
|
||||
this.tempCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 获取智能质量设置
|
||||
const quality = this._getOptimalQuality(imageData, true);
|
||||
|
||||
// 直接更新fabric对象的图像源(使用PNG格式保持质量)
|
||||
const targetElement = this.targetObject._element;
|
||||
|
||||
// 方案1: 直接设置src属性(最高性能)
|
||||
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
|
||||
|
||||
if (targetElement.src !== dataURL) {
|
||||
targetElement.src = dataURL;
|
||||
|
||||
// 关键优化:直接设置fabric对象为脏状态,但不立即渲染
|
||||
// this.targetObject.dirty = false; // 标记为不需要立即渲染
|
||||
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
|
||||
// this.renderingScheduled = false; // 重置渲染调度状态
|
||||
this?.scheduleRender?.(); // 调度一次渲染
|
||||
// 使用requestAnimationFrame进行批量渲染优化
|
||||
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
|
||||
// this.renderingScheduled = true;
|
||||
// requestIdleCallback(() => {
|
||||
// this.canvas.renderAll();
|
||||
// this.renderingScheduled = false;
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
console.warn(
|
||||
"=================快速更新液化效果时,图像数据未变化,跳过更新"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("快速更新液化效果失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(imageData) {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整更新 - 创建新的fabric对象
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _fullUpdate(imageData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
|
||||
// 如果DataURL没有变化,跳过更新
|
||||
if (this.cachedDataURL === dataURL) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedDataURL = dataURL;
|
||||
|
||||
// 创建新的fabric图像对象,保持最高质量
|
||||
fabric.Image.fromURL(
|
||||
dataURL,
|
||||
(newImg) => {
|
||||
try {
|
||||
if (!this.targetObject) {
|
||||
console.warn("目标对象为空,跳过更新");
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存原对象信息用于智能查找
|
||||
const originalObjId = this.targetObject.id;
|
||||
const originalObjLayerId = this.targetObject.layerId;
|
||||
|
||||
// 保留原对象的所有变换属性
|
||||
const originalObj = this.targetObject;
|
||||
newImg.set({
|
||||
left: originalObj.left,
|
||||
top: originalObj.top,
|
||||
scaleX: originalObj.scaleX,
|
||||
scaleY: originalObj.scaleY,
|
||||
angle: originalObj.angle,
|
||||
flipX: originalObj.flipX,
|
||||
flipY: originalObj.flipY,
|
||||
opacity: originalObj.opacity,
|
||||
originX: originalObj.originX,
|
||||
originY: originalObj.originY,
|
||||
id: originalObj.id,
|
||||
name: originalObj.name,
|
||||
layerId: originalObj.layerId,
|
||||
selected: false,
|
||||
evented: originalObj.evented,
|
||||
});
|
||||
|
||||
// 临时禁用画布自动渲染
|
||||
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
// 智能查找和替换canvas上的对象
|
||||
const allObjects = this.canvas.getObjects();
|
||||
let targetIndex = allObjects.indexOf(originalObj);
|
||||
|
||||
// 如果直接查找失败,尝试通过ID查找
|
||||
if (targetIndex === -1 && originalObjId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.id === originalObjId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过ID找到目标对象: ${originalObjId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过ID查找仍然失败,尝试通过图层ID查找
|
||||
if (targetIndex === -1 && originalObjLayerId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.layerId === originalObjLayerId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
// 找到目标对象,执行替换
|
||||
this.canvas.remove(this.targetObject);
|
||||
this.canvas.insertAt(newImg, targetIndex);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 更新目标对象引用
|
||||
this.targetObject = newImg;
|
||||
|
||||
// 一次性重新渲染画布
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
|
||||
resolve(newImg);
|
||||
} else {
|
||||
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
|
||||
console.warn(
|
||||
"在画布中找不到目标对象,可能已被其他操作移除或替换"
|
||||
);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 尝试添加新对象到画布末尾
|
||||
this.canvas.add(newImg);
|
||||
this.targetObject = newImg;
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("🔄 已将新对象添加到画布末尾");
|
||||
resolve(newImg);
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
console.error("更新fabric对象时出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
); // 确保跨域支持
|
||||
} catch (error) {
|
||||
console.error("完整更新过程出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理待处理的图像数据
|
||||
* 在拖拽结束后调用,处理可能积压的更新
|
||||
*/
|
||||
async processPendingUpdates() {
|
||||
if (this.pendingImageData && !this.isUpdating) {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
await this._fullUpdate(this.pendingImageData);
|
||||
this.pendingImageData = null;
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this.targetObject = null;
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.updateQueue.length = 0;
|
||||
|
||||
// 清理临时canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
this.tempCanvas = null;
|
||||
this.tempCtx = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
* @returns {Object} 当前的fabric对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制进行完整更新
|
||||
* @param {ImageData} imageData 图像数据
|
||||
*/
|
||||
async forceFullUpdate(imageData) {
|
||||
return this._fullUpdate(imageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用拖拽模式 - 暂停渲染以提高性能
|
||||
*/
|
||||
enableDragMode() {
|
||||
this.config.skipRenderDuringDrag = true;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
console.log("🚀 启用拖拽优化模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用拖拽模式 - 恢复正常渲染
|
||||
*/
|
||||
disableDragMode() {
|
||||
this.config.skipRenderDuringDrag = false;
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 执行一次完整渲染
|
||||
this.canvas.renderAll();
|
||||
console.log("✅ 恢复正常渲染模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图像质量
|
||||
* @param {Number} quality 质量值 (0.1-1.0)
|
||||
*/
|
||||
setImageQuality(quality) {
|
||||
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的批量渲染方法
|
||||
*/
|
||||
scheduleRender() {
|
||||
if (!this.renderingScheduled) {
|
||||
this.renderingScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.canvas.renderAll();
|
||||
this.renderingScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
// 恢复canvas设置
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 清理缓存
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
|
||||
// 清理canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
}
|
||||
|
||||
if (this.highQualityCanvas) {
|
||||
this.highQualityCanvas.width = 0;
|
||||
this.highQualityCanvas.height = 0;
|
||||
}
|
||||
|
||||
console.log("🧹 液化实时更新器资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 液化面板状态管理器
|
||||
* 负责管理液化操作的状态、性能优化和用户反馈
|
||||
*/
|
||||
export class LiquifyStateManager {
|
||||
constructor(canvas, realtimeUpdater) {
|
||||
this.canvas = canvas;
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
|
||||
// 状态管理
|
||||
this.isOperating = false;
|
||||
this.isDragging = false;
|
||||
this.operationCount = 0;
|
||||
this.startTime = null;
|
||||
|
||||
// 性能监控
|
||||
this.performanceMetrics = {
|
||||
totalOperations: 0,
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
maxTime: 0,
|
||||
minTime: Infinity,
|
||||
lastOperationTime: 0,
|
||||
};
|
||||
|
||||
// 用户反馈
|
||||
this.feedbackEnabled = true;
|
||||
this.cursorCache = new Map();
|
||||
|
||||
// 设备性能检测
|
||||
this.devicePerformance = this._detectDevicePerformance();
|
||||
|
||||
console.log(
|
||||
"🎯 液化状态管理器已初始化,设备性能等级:",
|
||||
this.devicePerformance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作
|
||||
*/
|
||||
startOperation() {
|
||||
if (this.isOperating) return;
|
||||
|
||||
this.isOperating = true;
|
||||
this.startTime = performance.now();
|
||||
|
||||
// 根据设备性能调整设置
|
||||
this._adjustPerformanceSettings();
|
||||
|
||||
// 显示操作反馈
|
||||
this._showOperationFeedback();
|
||||
|
||||
console.log("🚀 开始液化操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始拖拽
|
||||
*/
|
||||
startDrag() {
|
||||
if (this.isDragging) return;
|
||||
|
||||
this.isDragging = true;
|
||||
|
||||
// 优化拖拽性能
|
||||
this.realtimeUpdater?.enableDragMode();
|
||||
|
||||
// 更新鼠标样式
|
||||
this._updateCursor("liquifying");
|
||||
|
||||
// 禁用不必要的画布功能
|
||||
this._disableCanvasFeatures();
|
||||
|
||||
console.log("🖱️ 开始拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束拖拽
|
||||
*/
|
||||
async endDrag() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
// 恢复鼠标样式
|
||||
this._updateCursor("default");
|
||||
|
||||
// 恢复画布功能
|
||||
this._enableCanvasFeatures();
|
||||
|
||||
// 处理待处理的更新
|
||||
if (this.realtimeUpdater) {
|
||||
try {
|
||||
await this.realtimeUpdater.processPendingUpdates();
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isDragging = false;
|
||||
// 恢复正常模式
|
||||
this.realtimeUpdater?.disableDragMode();
|
||||
|
||||
// 结束液化操作 添加结果到命令中 更新当前激活图层对象
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 结束拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endOperation() {
|
||||
if (!this.isOperating) return;
|
||||
|
||||
const operationTime = performance.now() - this.startTime;
|
||||
this._updatePerformanceMetrics(operationTime);
|
||||
|
||||
this.isOperating = false;
|
||||
this.operationCount++;
|
||||
|
||||
// 隐藏操作反馈
|
||||
this._hideOperationFeedback();
|
||||
|
||||
console.log(`⏱️ 液化操作完成,耗时: ${operationTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录单次变形操作
|
||||
*/
|
||||
recordDeformation(operationTime) {
|
||||
this.performanceMetrics.totalOperations++;
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime /
|
||||
this.performanceMetrics.totalOperations;
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.lastOperationTime = operationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作性能指标
|
||||
* @param {Object} metrics 性能指标对象
|
||||
*/
|
||||
recordOperationMetrics(metrics) {
|
||||
const {
|
||||
operationTime,
|
||||
operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
} = metrics;
|
||||
|
||||
// 记录基础性能数据
|
||||
this.recordDeformation(operationTime);
|
||||
|
||||
// 记录详细操作信息
|
||||
this.performanceMetrics.lastOperation = {
|
||||
type: operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 根据性能数据动态调整设置
|
||||
this._adaptivePerformanceOptimization(operationTime);
|
||||
|
||||
console.log(
|
||||
`📊 记录性能指标: ${operationType}/${mode}, 耗时: ${operationTime.toFixed(
|
||||
2
|
||||
)}ms, 渲染模式: ${renderMode}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应性能优化
|
||||
* @param {Number} operationTime 操作耗时
|
||||
* @private
|
||||
*/
|
||||
_adaptivePerformanceOptimization(operationTime) {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
// 如果操作耗时过长,动态降低质量或增加节流时间
|
||||
if (operationTime > 50 && this.devicePerformance !== "high") {
|
||||
// 降低图像质量
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality > 0.7) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.max(0.7, currentQuality - 0.1)
|
||||
);
|
||||
console.log("⚡ 自动降低图像质量以提升性能");
|
||||
}
|
||||
|
||||
// 增加节流时间
|
||||
if (this.realtimeUpdater.config.throttleTime < 33) {
|
||||
this.realtimeUpdater.config.throttleTime = Math.min(
|
||||
33,
|
||||
this.realtimeUpdater.config.throttleTime + 8
|
||||
);
|
||||
console.log("⏱️ 自动增加节流时间以提升性能");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果性能很好,可以适当提高质量
|
||||
if (operationTime < 20 && this.devicePerformance === "high") {
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality < 1.0) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.min(1.0, currentQuality + 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
getPerformanceReport() {
|
||||
return {
|
||||
...this.performanceMetrics,
|
||||
devicePerformance: this.devicePerformance,
|
||||
fps: this._calculateFPS(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户反馈
|
||||
*/
|
||||
setFeedbackEnabled(enabled) {
|
||||
this.feedbackEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this._enableCanvasFeatures();
|
||||
this._updateCursor("default");
|
||||
this.cursorCache.clear();
|
||||
|
||||
console.log("🧹 液化状态管理器已清理");
|
||||
}
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
/**
|
||||
* 检测设备性能
|
||||
*/
|
||||
_detectDevicePerformance() {
|
||||
// 检测硬件并发数
|
||||
const cores = navigator.hardwareConcurrency || 4;
|
||||
|
||||
// 检测内存
|
||||
const memory = navigator.deviceMemory || 4;
|
||||
|
||||
// 检测连接类型
|
||||
const connection = navigator.connection;
|
||||
const effectiveType = connection?.effectiveType || "4g";
|
||||
|
||||
// 简单的性能评分算法
|
||||
let score = 0;
|
||||
score += cores * 10;
|
||||
score += memory * 5;
|
||||
|
||||
if (effectiveType === "4g") score += 10;
|
||||
else if (effectiveType === "3g") score += 5;
|
||||
|
||||
// 性能等级
|
||||
if (score >= 70) return "high";
|
||||
if (score >= 40) return "medium";
|
||||
return "low";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备性能调整设置
|
||||
*/
|
||||
_adjustPerformanceSettings() {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
switch (this.devicePerformance) {
|
||||
case "high":
|
||||
this.realtimeUpdater.setImageQuality(1.0);
|
||||
this.realtimeUpdater.config.throttleTime = 8; // 120fps
|
||||
break;
|
||||
case "medium":
|
||||
this.realtimeUpdater.setImageQuality(0.9);
|
||||
this.realtimeUpdater.config.throttleTime = 16; // 60fps
|
||||
break;
|
||||
case "low":
|
||||
this.realtimeUpdater.setImageQuality(0.8);
|
||||
this.realtimeUpdater.config.throttleTime = 33; // 30fps
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示操作反馈
|
||||
*/
|
||||
_showOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
// 添加视觉反馈(可以是加载动画、进度条等)
|
||||
document.body.style.cursor = "wait";
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏操作反馈
|
||||
*/
|
||||
_hideOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
document.body.style.cursor = "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新鼠标样式
|
||||
*/
|
||||
_updateCursor(type) {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
const cursors = {
|
||||
default: "default",
|
||||
liquifying: "crosshair",
|
||||
wait: "wait",
|
||||
"not-allowed": "not-allowed",
|
||||
};
|
||||
|
||||
const cursor = cursors[type] || "default";
|
||||
|
||||
if (this.canvas && this.canvas.upperCanvasEl) {
|
||||
this.canvas.upperCanvasEl.style.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用画布功能以提高性能
|
||||
*/
|
||||
_disableCanvasFeatures() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 保存原始设置
|
||||
this._originalSettings = {
|
||||
renderOnAddRemove: this.canvas.renderOnAddRemove,
|
||||
skipOffscreen: this.canvas.skipOffscreen,
|
||||
enableRetinaScaling: this.canvas.enableRetinaScaling,
|
||||
};
|
||||
|
||||
// 应用性能优化设置
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
this.canvas.skipOffscreen = true;
|
||||
|
||||
// 低性能设备关闭高分辨率支持
|
||||
if (this.devicePerformance === "low") {
|
||||
this.canvas.enableRetinaScaling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布功能
|
||||
*/
|
||||
_enableCanvasFeatures() {
|
||||
if (!this.canvas || !this._originalSettings) return;
|
||||
|
||||
// 恢复原始设置
|
||||
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
|
||||
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
|
||||
this.canvas.enableRetinaScaling =
|
||||
this._originalSettings.enableRetinaScaling;
|
||||
|
||||
this._originalSettings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能指标
|
||||
*/
|
||||
_updatePerformanceMetrics(operationTime) {
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime / (this.operationCount + 1);
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算FPS
|
||||
*/
|
||||
_calculateFPS() {
|
||||
if (this.performanceMetrics.averageTime === 0) return 0;
|
||||
return Math.round(1000 / this.performanceMetrics.averageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成性能建议
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
if (this.performanceMetrics.averageTime > 50) {
|
||||
recommendations.push("操作响应较慢,建议降低图像尺寸或关闭高质量模式");
|
||||
}
|
||||
|
||||
if (this.devicePerformance === "low") {
|
||||
recommendations.push("检测到低性能设备,已自动启用性能优化模式");
|
||||
}
|
||||
|
||||
const fps = this._calculateFPS();
|
||||
if (fps < 30) {
|
||||
recommendations.push("帧率较低,建议减少同时进行的操作或降低液化强度");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 小地图管理器类
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { generateId } from "../../utils/helper";
|
||||
import { OperationType } from "../../utils/layerHelper";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
/**
|
||||
* 液化功能集成测试
|
||||
* 用于在浏览器环境中测试完整的液化工作流程
|
||||
*/
|
||||
|
||||
import { LiquifyCPUManager } from "../managers/liquify/LiquifyCPUManager.js";
|
||||
import { LiquifyWebGLManager } from "../managers/liquify/LiquifyWebGLManager.js";
|
||||
import { LiquifyRealTimeUpdater } from "../managers/liquify/LiquifyRealTimeUpdater.js";
|
||||
import { EnhancedLiquifyManager } from "../managers/liquify/EnhancedLiquifyManager.js";
|
||||
|
||||
// 集成测试结果
|
||||
let testResults = {
|
||||
totalTests: 0,
|
||||
passedTests: 0,
|
||||
failedTests: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建测试图像数据
|
||||
*/
|
||||
function createTestImageData(width = 100, height = 100) {
|
||||
const imageData = new ImageData(width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
// 创建一个简单的渐变图案用于测试
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
data[index] = (x / width) * 255; // Red
|
||||
data[index + 1] = (y / height) * 255; // Green
|
||||
data[index + 2] = 128; // Blue
|
||||
data[index + 3] = 255; // Alpha
|
||||
}
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试Canvas
|
||||
*/
|
||||
function createTestCanvas(width = 200, height = 200) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟Fabric对象
|
||||
*/
|
||||
function createMockFabricObject(imageData) {
|
||||
return {
|
||||
left: 50,
|
||||
top: 50,
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
_element: null,
|
||||
calcTransformMatrix: function () {
|
||||
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
|
||||
},
|
||||
set: function (props) {
|
||||
Object.assign(this, props);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行单个测试
|
||||
*/
|
||||
async function runTest(testName, testFunction) {
|
||||
testResults.totalTests++;
|
||||
|
||||
try {
|
||||
console.log(`🧪 运行测试: ${testName}`);
|
||||
await testFunction();
|
||||
testResults.passedTests++;
|
||||
console.log(`✅ 测试通过: ${testName}`);
|
||||
} catch (error) {
|
||||
testResults.failedTests++;
|
||||
testResults.errors.push({ testName, error: error.message });
|
||||
console.error(`❌ 测试失败: ${testName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试CPU管理器基本功能
|
||||
*/
|
||||
async function testCPUManagerBasics() {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData();
|
||||
|
||||
// 初始化
|
||||
manager.initialize(testImageData);
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 设置模式
|
||||
manager.setMode("push");
|
||||
|
||||
// 应用变形
|
||||
const result = manager.applyDeformation(50, 50);
|
||||
|
||||
if (
|
||||
!result ||
|
||||
result.width !== testImageData.width ||
|
||||
result.height !== testImageData.height
|
||||
) {
|
||||
throw new Error("CPU管理器变形结果无效");
|
||||
}
|
||||
|
||||
// 测试不同模式
|
||||
const modes = [
|
||||
"push",
|
||||
"clockwise",
|
||||
"counterclockwise",
|
||||
"pinch",
|
||||
"expand",
|
||||
"reconstruct",
|
||||
];
|
||||
for (const mode of modes) {
|
||||
manager.setMode(mode);
|
||||
const modeResult = manager.applyDeformation(25, 25);
|
||||
if (!modeResult) {
|
||||
throw new Error(`模式 ${mode} 变形失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试WebGL管理器基本功能
|
||||
*/
|
||||
async function testWebGLManagerBasics() {
|
||||
const manager = new LiquifyWebGLManager();
|
||||
const testImageData = createTestImageData();
|
||||
|
||||
try {
|
||||
// 初始化
|
||||
manager.initialize(testImageData);
|
||||
|
||||
// 检查WebGL是否可用
|
||||
if (!manager.initialized) {
|
||||
console.warn("WebGL不可用,跳过WebGL测试");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 设置模式
|
||||
manager.setMode("push");
|
||||
|
||||
// 应用变形
|
||||
const result = manager.applyDeformation(50, 50);
|
||||
|
||||
if (
|
||||
!result ||
|
||||
result.width !== testImageData.width ||
|
||||
result.height !== testImageData.height
|
||||
) {
|
||||
throw new Error("WebGL管理器变形结果无效");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes("WebGL")) {
|
||||
console.warn("WebGL不支持,跳过WebGL测试");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试实时更新器功能
|
||||
*/
|
||||
async function testRealTimeUpdater() {
|
||||
const canvas = createTestCanvas();
|
||||
const testImageData = createTestImageData();
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 模拟fabric Canvas
|
||||
const fabricCanvas = {
|
||||
getObjects: () => [mockFabricObject],
|
||||
remove: () => {},
|
||||
insertAt: () => {},
|
||||
renderAll: () => {},
|
||||
};
|
||||
|
||||
const updater = new LiquifyRealTimeUpdater(fabricCanvas);
|
||||
|
||||
// 设置目标对象
|
||||
updater.setTargetObject(mockFabricObject);
|
||||
|
||||
// 测试快速更新
|
||||
await updater.updateImage(testImageData, true);
|
||||
|
||||
// 测试完整更新
|
||||
await updater.updateImage(testImageData, false);
|
||||
|
||||
// 测试待处理更新
|
||||
updater.pendingImageData = testImageData;
|
||||
await updater.processPendingUpdates();
|
||||
|
||||
// 清理
|
||||
updater.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试增强液化管理器
|
||||
*/
|
||||
async function testEnhancedLiquifyManager() {
|
||||
const manager = new EnhancedLiquifyManager();
|
||||
const testImageData = createTestImageData();
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 初始化
|
||||
const result = await manager.prepareForLiquify(mockFabricObject);
|
||||
|
||||
if (!result || !result.originalImageData) {
|
||||
throw new Error("增强液化管理器初始化失败");
|
||||
}
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 应用液化
|
||||
const liquifyResult = await manager.applyLiquify(
|
||||
mockFabricObject,
|
||||
"push",
|
||||
{ size: 50, pressure: 0.5, distortion: 0.3, power: 0.8 },
|
||||
50,
|
||||
50
|
||||
);
|
||||
|
||||
if (!liquifyResult) {
|
||||
throw new Error("液化应用失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试坐标转换准确性
|
||||
*/
|
||||
async function testCoordinateConversion() {
|
||||
const testImageData = createTestImageData(200, 200);
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 测试不同的缩放情况
|
||||
const testCases = [
|
||||
{ scaleX: 1, scaleY: 1, flipX: false, flipY: false },
|
||||
{ scaleX: 2, scaleY: 2, flipX: false, flipY: false },
|
||||
{ scaleX: 0.5, scaleY: 0.5, flipX: false, flipY: false },
|
||||
{ scaleX: 1, scaleY: 1, flipX: true, flipY: false },
|
||||
{ scaleX: 1, scaleY: 1, flipX: false, flipY: true },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
Object.assign(mockFabricObject, testCase);
|
||||
|
||||
// 模拟坐标转换函数(简化版)
|
||||
const fabricX = 100,
|
||||
fabricY = 100;
|
||||
|
||||
// 创建变换矩阵模拟
|
||||
const transform = mockFabricObject.calcTransformMatrix();
|
||||
|
||||
// 基本的坐标边界检查
|
||||
const localX = (fabricX - mockFabricObject.left) / mockFabricObject.scaleX;
|
||||
const localY = (fabricY - mockFabricObject.top) / mockFabricObject.scaleY;
|
||||
|
||||
if (
|
||||
localX < -mockFabricObject.width / 2 ||
|
||||
localX > mockFabricObject.width / 2 ||
|
||||
localY < -mockFabricObject.height / 2 ||
|
||||
localY > mockFabricObject.height / 2
|
||||
) {
|
||||
// 坐标在对象外部,这是正常情况
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换到图像坐标
|
||||
let imageX =
|
||||
(localX + mockFabricObject.width / 2) *
|
||||
(testImageData.width / mockFabricObject.width);
|
||||
let imageY =
|
||||
(localY + mockFabricObject.height / 2) *
|
||||
(testImageData.height / mockFabricObject.height);
|
||||
|
||||
// 处理翻转
|
||||
if (mockFabricObject.flipX) {
|
||||
imageX = testImageData.width - imageX;
|
||||
}
|
||||
if (mockFabricObject.flipY) {
|
||||
imageY = testImageData.height - imageY;
|
||||
}
|
||||
|
||||
// 验证结果在合理范围内
|
||||
if (
|
||||
imageX < 0 ||
|
||||
imageX >= testImageData.width ||
|
||||
imageY < 0 ||
|
||||
imageY >= testImageData.height
|
||||
) {
|
||||
throw new Error(`坐标转换结果超出图像范围: (${imageX}, ${imageY})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试性能表现
|
||||
*/
|
||||
async function testPerformance() {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData(400, 400); // 更大的图像
|
||||
|
||||
manager.initialize(testImageData);
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
manager.setMode("push");
|
||||
|
||||
// 测试多次操作的性能
|
||||
const startTime = performance.now();
|
||||
const operationCount = 100;
|
||||
|
||||
for (let i = 0; i < operationCount; i++) {
|
||||
const x = Math.random() * testImageData.width;
|
||||
const y = Math.random() * testImageData.height;
|
||||
manager.applyDeformation(x, y);
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const avgTime = totalTime / operationCount;
|
||||
|
||||
console.log(
|
||||
`性能测试结果: ${operationCount} 次操作,总耗时 ${totalTime.toFixed(
|
||||
2
|
||||
)}ms,平均 ${avgTime.toFixed(2)}ms/次`
|
||||
);
|
||||
|
||||
// 验证性能阈值(每次操作不应超过50ms)
|
||||
if (avgTime > 50) {
|
||||
throw new Error(
|
||||
`性能不达标:平均操作时间 ${avgTime.toFixed(2)}ms 超过阈值 50ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试内存管理
|
||||
*/
|
||||
async function testMemoryManagement() {
|
||||
const initialMemory = performance.memory
|
||||
? performance.memory.usedJSHeapSize
|
||||
: 0;
|
||||
|
||||
// 创建多个管理器实例
|
||||
const managers = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData(200, 200);
|
||||
manager.initialize(testImageData);
|
||||
managers.push(manager);
|
||||
}
|
||||
|
||||
// 销毁所有管理器
|
||||
for (const manager of managers) {
|
||||
if (manager.destroy) {
|
||||
manager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 强制垃圾回收(如果可用)
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
|
||||
const finalMemory = performance.memory
|
||||
? performance.memory.usedJSHeapSize
|
||||
: 0;
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
|
||||
console.log(
|
||||
`内存测试: 初始 ${(initialMemory / 1024 / 1024).toFixed(2)}MB, 最终 ${(
|
||||
finalMemory /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)}MB, 增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
|
||||
);
|
||||
|
||||
// 验证内存增长不超过10MB(基本的内存泄漏检查)
|
||||
if (memoryIncrease > 10 * 1024 * 1024) {
|
||||
console.warn(
|
||||
`潜在内存泄漏: 内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行所有集成测试
|
||||
*/
|
||||
export async function runLiquifyIntegrationTests() {
|
||||
console.log("🚀 开始液化功能集成测试...");
|
||||
|
||||
// 重置测试结果
|
||||
testResults = {
|
||||
totalTests: 0,
|
||||
passedTests: 0,
|
||||
failedTests: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
// 基础功能测试
|
||||
await runTest("CPU管理器基本功能", testCPUManagerBasics);
|
||||
await runTest("WebGL管理器基本功能", testWebGLManagerBasics);
|
||||
await runTest("实时更新器功能", testRealTimeUpdater);
|
||||
await runTest("增强液化管理器", testEnhancedLiquifyManager);
|
||||
|
||||
// 高级功能测试
|
||||
await runTest("坐标转换准确性", testCoordinateConversion);
|
||||
await runTest("性能表现", testPerformance);
|
||||
await runTest("内存管理", testMemoryManagement);
|
||||
} catch (error) {
|
||||
console.error("集成测试出现严重错误:", error);
|
||||
}
|
||||
|
||||
// 输出测试结果
|
||||
console.log("\n📊 液化功能集成测试结果:");
|
||||
console.log(`总测试数: ${testResults.totalTests}`);
|
||||
console.log(`通过: ${testResults.passedTests}`);
|
||||
console.log(`失败: ${testResults.failedTests}`);
|
||||
console.log(
|
||||
`成功率: ${(
|
||||
(testResults.passedTests / testResults.totalTests) *
|
||||
100
|
||||
).toFixed(1)}%`
|
||||
);
|
||||
|
||||
if (testResults.errors.length > 0) {
|
||||
console.log("\n❌ 失败的测试:");
|
||||
testResults.errors.forEach(({ testName, error }) => {
|
||||
console.log(` - ${testName}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
return testResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在浏览器控制台中运行测试
|
||||
*/
|
||||
if (typeof window !== "undefined") {
|
||||
window.runLiquifyIntegrationTests = runLiquifyIntegrationTests;
|
||||
console.log("💡 使用 window.runLiquifyIntegrationTests() 运行液化集成测试");
|
||||
}
|
||||
222
src/component/Canvas/CanvasEditor/tests/liquifyTests.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 液化功能测试和验证
|
||||
* 用于测试液化推拉算法和坐标转换修复
|
||||
*/
|
||||
|
||||
// 测试坐标转换函数
|
||||
export function testCoordinateConversion() {
|
||||
console.log("开始测试坐标转换...");
|
||||
|
||||
// 模拟fabric对象
|
||||
const mockFabricObject = {
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 200,
|
||||
height: 200,
|
||||
scaleX: 1.5,
|
||||
scaleY: 1.5,
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
calcTransformMatrix: function () {
|
||||
// 简化的变换矩阵计算
|
||||
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
|
||||
},
|
||||
};
|
||||
|
||||
// 模拟图像数据
|
||||
const mockImageData = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
};
|
||||
|
||||
// 测试不同的画布坐标
|
||||
const testCoordinates = [
|
||||
{ x: 150, y: 150 }, // 左上角
|
||||
{ x: 200, y: 200 }, // 中心
|
||||
{ x: 250, y: 250 }, // 右下角
|
||||
];
|
||||
|
||||
testCoordinates.forEach((coord, index) => {
|
||||
console.log(`测试坐标 ${index + 1}: (${coord.x}, ${coord.y})`);
|
||||
|
||||
// 这里应该调用实际的坐标转换函数
|
||||
// const imageCoords = _convertFabricCoordsToImageCoords(coord.x, coord.y);
|
||||
|
||||
// 模拟转换结果
|
||||
const expectedImageX =
|
||||
((coord.x - mockFabricObject.left) / mockFabricObject.scaleX +
|
||||
mockFabricObject.width / 2) *
|
||||
(mockImageData.width / mockFabricObject.width);
|
||||
const expectedImageY =
|
||||
((coord.y - mockFabricObject.top) / mockFabricObject.scaleY +
|
||||
mockFabricObject.height / 2) *
|
||||
(mockImageData.height / mockFabricObject.height);
|
||||
|
||||
console.log(
|
||||
` 预期图像坐标: (${expectedImageX.toFixed(2)}, ${expectedImageY.toFixed(
|
||||
2
|
||||
)})`
|
||||
);
|
||||
});
|
||||
|
||||
console.log("坐标转换测试完成");
|
||||
}
|
||||
|
||||
// 测试推拉算法性能
|
||||
export function testPushAlgorithmPerformance() {
|
||||
console.log("开始测试推拉算法性能...");
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// 模拟连续的推拉操作
|
||||
const operations = 100;
|
||||
const movements = [];
|
||||
|
||||
for (let i = 0; i < operations; i++) {
|
||||
// 模拟鼠标移动
|
||||
const x = 200 + Math.sin(i * 0.1) * 50;
|
||||
const y = 200 + Math.cos(i * 0.1) * 50;
|
||||
|
||||
movements.push({ x, y, timestamp: Date.now() });
|
||||
|
||||
// 模拟液化计算
|
||||
const movementLength =
|
||||
i > 0
|
||||
? Math.sqrt(
|
||||
Math.pow(x - movements[i - 1].x, 2) +
|
||||
Math.pow(y - movements[i - 1].y, 2)
|
||||
)
|
||||
: 0;
|
||||
|
||||
if (movementLength > 0.5) {
|
||||
// 模拟变形计算
|
||||
const pressure = 0.8;
|
||||
const power = 0.8;
|
||||
const velocityFactor = Math.min(movementLength * 0.1, 1.0);
|
||||
const pushStrength = pressure * power * velocityFactor * 0.5;
|
||||
|
||||
// 记录计算结果
|
||||
movements[i].strength = pushStrength;
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const avgTimePerOperation = totalTime / operations;
|
||||
|
||||
console.log(`推拉算法性能测试结果:`);
|
||||
console.log(` 总操作数: ${operations}`);
|
||||
console.log(` 总耗时: ${totalTime.toFixed(2)}ms`);
|
||||
console.log(` 平均每次操作耗时: ${avgTimePerOperation.toFixed(2)}ms`);
|
||||
console.log(` 预估FPS: ${(1000 / avgTimePerOperation).toFixed(1)}fps`);
|
||||
|
||||
return {
|
||||
totalTime,
|
||||
avgTimePerOperation,
|
||||
estimatedFps: 1000 / avgTimePerOperation,
|
||||
};
|
||||
}
|
||||
|
||||
// 测试缩放一致性
|
||||
export function testScaleConsistency() {
|
||||
console.log("开始测试缩放一致性...");
|
||||
|
||||
// 模拟不同缩放比例的对象
|
||||
const scaleFactors = [0.5, 1.0, 1.5, 2.0, 2.5];
|
||||
|
||||
scaleFactors.forEach((scale) => {
|
||||
console.log(`测试缩放比例: ${scale}`);
|
||||
|
||||
// 模拟fabric对象
|
||||
const fabricObject = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
left: 300,
|
||||
top: 300,
|
||||
};
|
||||
|
||||
// 模拟图像数据(原始尺寸)
|
||||
const imageData = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
};
|
||||
|
||||
// 测试坐标转换
|
||||
const canvasCoord = { x: 350, y: 350 }; // 画布坐标
|
||||
|
||||
// 计算预期的图像坐标(修复后的逻辑)
|
||||
const localX = (canvasCoord.x - fabricObject.left) / fabricObject.scaleX;
|
||||
const localY = (canvasCoord.y - fabricObject.top) / fabricObject.scaleY;
|
||||
|
||||
const imageX =
|
||||
(localX + fabricObject.width / 2) *
|
||||
(imageData.width / fabricObject.width);
|
||||
const imageY =
|
||||
(localY + fabricObject.height / 2) *
|
||||
(imageData.height / fabricObject.height);
|
||||
|
||||
console.log(` 画布坐标: (${canvasCoord.x}, ${canvasCoord.y})`);
|
||||
console.log(` 本地坐标: (${localX.toFixed(2)}, ${localY.toFixed(2)})`);
|
||||
console.log(` 图像坐标: (${imageX.toFixed(2)}, ${imageY.toFixed(2)})`);
|
||||
|
||||
// 验证图像坐标是否在合理范围内
|
||||
const isValid =
|
||||
imageX >= 0 &&
|
||||
imageX < imageData.width &&
|
||||
imageY >= 0 &&
|
||||
imageY < imageData.height;
|
||||
console.log(` 坐标有效性: ${isValid ? "✓" : "✗"}`);
|
||||
});
|
||||
|
||||
console.log("缩放一致性测试完成");
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
export function runAllTests() {
|
||||
console.log("=== 液化功能测试开始 ===");
|
||||
|
||||
try {
|
||||
testCoordinateConversion();
|
||||
console.log("");
|
||||
|
||||
const perfResult = testPushAlgorithmPerformance();
|
||||
console.log("");
|
||||
|
||||
testScaleConsistency();
|
||||
console.log("");
|
||||
|
||||
console.log("=== 液化功能测试完成 ===");
|
||||
console.log(
|
||||
`推荐配置: 节流时间 ${Math.max(
|
||||
16,
|
||||
perfResult.avgTimePerOperation * 2
|
||||
).toFixed(0)}ms`
|
||||
);
|
||||
|
||||
return {
|
||||
coordinateConversion: "通过",
|
||||
performance: perfResult,
|
||||
scaleConsistency: "通过",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("测试过程中出现错误:", error);
|
||||
return {
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 在浏览器控制台中运行测试
|
||||
if (typeof window !== "undefined") {
|
||||
window.liquifyTests = {
|
||||
testCoordinateConversion,
|
||||
testPushAlgorithmPerformance,
|
||||
testScaleConsistency,
|
||||
runAllTests,
|
||||
};
|
||||
|
||||
console.log("液化测试工具已加载,可通过 window.liquifyTests 访问");
|
||||
}
|
||||
820
src/component/Canvas/CanvasEditor/utils/LayerSort.js
Normal file
@@ -0,0 +1,820 @@
|
||||
import { ReorderChildLayersCommand } from "../commands/LayerCommands";
|
||||
import { optimizeCanvasRendering } from "./helper";
|
||||
import { findLayerRecursively, LayerType } from "./layerHelper";
|
||||
|
||||
/**
|
||||
* 图层排序工具类
|
||||
* 提供图层排序、重新排列画布对象等功能
|
||||
* 基于fabric.js 5.3.1版本开发
|
||||
*/
|
||||
export class LayerSort {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric.js画布实例
|
||||
* @param {Object} layers 图层数组响应式引用
|
||||
*/
|
||||
constructor(canvas, layers, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.layers = layers;
|
||||
this.commandManager = options.commandManager || null; // 命令管理器(可选)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新排列画布上的对象以匹配图层顺序
|
||||
* 使用 fabric.js 的 moveTo 方法直接调整对象层级,无需清空画布
|
||||
*/
|
||||
async rearrangeObjects() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
if (canvasObjects.length === 0) return;
|
||||
|
||||
// 使用画布渲染优化
|
||||
await optimizeCanvasRendering(this.canvas, () => {
|
||||
// 计算每个对象应该在的 z-index 位置
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
// 按照新的 z-index 排序对象
|
||||
const sortedObjects = canvasObjects
|
||||
.map((obj) => ({
|
||||
object: obj,
|
||||
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
|
||||
}))
|
||||
.filter((item) => item.targetZIndex >= 0) // 过滤掉无效对象
|
||||
.sort((a, b) => a.targetZIndex - b.targetZIndex);
|
||||
|
||||
// 使用 fabric.js 的 moveTo 方法重新排序
|
||||
sortedObjects.forEach((item, index) => {
|
||||
const currentIndex = this.canvas.getObjects().indexOf(item.object);
|
||||
if (currentIndex !== index && currentIndex !== -1) {
|
||||
// 将对象移动到正确的位置
|
||||
this.canvas.moveTo(item.object, index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算每个对象在画布中应该的 z-index
|
||||
* 考虑图层类型、组结构和子图层
|
||||
* @returns {Map} 对象ID到z-index的映射
|
||||
*/
|
||||
calculateObjectZIndexes() {
|
||||
const zIndexMap = new Map();
|
||||
let currentZIndex = 0;
|
||||
|
||||
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
|
||||
for (let i = this.layers.value.length - 1; i >= 0; i--) {
|
||||
const layer = this.layers.value[i];
|
||||
|
||||
// 跳过不可见图层
|
||||
if (!layer.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理不同类型的图层
|
||||
if (layer.isBackground && layer.fabricObject) {
|
||||
// 背景图层对象放在最底层
|
||||
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||
} else if (layer.isFixed && layer.fabricObjects) {
|
||||
// 固定图层对象
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
});
|
||||
} else if (!layer.isBackground && !layer.isFixed) {
|
||||
// 普通图层
|
||||
currentZIndex = this.processLayerObjects(
|
||||
layer,
|
||||
currentZIndex,
|
||||
zIndexMap
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return zIndexMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图层对象,包括组和子图层的情况
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {number} currentZIndex 当前z-index
|
||||
* @param {Map} zIndexMap z-index映射表
|
||||
* @returns {number} 更新后的z-index
|
||||
*/
|
||||
processLayerObjects(layer, currentZIndex, zIndexMap) {
|
||||
// 检查是否有子图层(组图层)
|
||||
if (layer.children?.length > 0) {
|
||||
// 处理每个子图层
|
||||
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
|
||||
for (let i = layer.children.length - 1; i >= 0; i--) {
|
||||
const childLayer = layer.children[i];
|
||||
|
||||
// 跳过不可见图层
|
||||
if (!childLayer.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = childLayer.fabricObjects.length - 1; j >= 0; j--) {
|
||||
const obj = childLayer.fabricObjects[j];
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图层本身的对象
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
for (let j = layer.fabricObjects.length - 1; j >= 0; j--) {
|
||||
const obj = layer.fabricObjects[j];
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentZIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定图层的子图层,按照正确顺序排列
|
||||
* @param {string} parentLayerId 父图层ID
|
||||
* @returns {Array} 子图层数组
|
||||
*/
|
||||
getChildLayersInOrder(parentLayerId) {
|
||||
// 获取所有子图层
|
||||
const childLayers =
|
||||
this.layers.value.filter((layer) => layer.id === parentLayerId)
|
||||
?.children || [];
|
||||
|
||||
return childLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取图层
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Object|null} 图层对象或null
|
||||
*/
|
||||
getLayerById(layerId) {
|
||||
if (!layerId || !this.layers.value) return null;
|
||||
return this.layers.value.find((layer) => layer.id === layerId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重新排列对象(异步版本,适用于大量对象)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rearrangeObjectsAsync() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
if (canvasObjects.length === 0) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 使用 requestAnimationFrame 进行异步处理
|
||||
requestAnimationFrame(() => {
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
try {
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
const sortedObjects = canvasObjects
|
||||
.map((obj) => ({
|
||||
object: obj,
|
||||
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
|
||||
}))
|
||||
.filter((item) => item.targetZIndex >= 0)
|
||||
.sort((a, b) => a.targetZIndex - b.targetZIndex);
|
||||
|
||||
// 分批处理,避免一次性处理太多对象
|
||||
const batchSize = LayerSortConstants.BATCH_SIZE;
|
||||
let currentBatch = 0;
|
||||
|
||||
const processBatch = () => {
|
||||
const start = currentBatch * batchSize;
|
||||
const end = Math.min(start + batchSize, sortedObjects.length);
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const item = sortedObjects[i];
|
||||
const currentIndex = this.canvas
|
||||
.getObjects()
|
||||
.indexOf(item.object);
|
||||
if (currentIndex !== i && currentIndex !== -1) {
|
||||
this.canvas.moveTo(item.object, i);
|
||||
}
|
||||
}
|
||||
|
||||
currentBatch++;
|
||||
|
||||
if (end < sortedObjects.length) {
|
||||
// 继续处理下一批
|
||||
requestAnimationFrame(processBatch);
|
||||
} else {
|
||||
// 所有批次处理完成
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
processBatch();
|
||||
} catch (error) {
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
console.error("重新排列对象时出错:", error);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象顺序是否正确
|
||||
* @returns {boolean} 顺序是否正确
|
||||
*/
|
||||
validateObjectOrder() {
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
for (let i = 0; i < canvasObjects.length - 1; i++) {
|
||||
const currentObj = canvasObjects[i];
|
||||
const nextObj = canvasObjects[i + 1];
|
||||
|
||||
const currentZIndex = objectZIndexMap.get(currentObj.id);
|
||||
const nextZIndex = objectZIndexMap.get(nextObj.id);
|
||||
|
||||
if (currentZIndex !== undefined && nextZIndex !== undefined) {
|
||||
if (currentZIndex > nextZIndex) {
|
||||
return false; // 顺序不正确
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 顺序正确
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层排序规则:背景图层 > 固定图层 > 普通图层
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Array} 排序后的图层数组
|
||||
*/
|
||||
sortLayers(layers = null) {
|
||||
const targetLayers = layers ?? this.layers.value;
|
||||
|
||||
return [...targetLayers].sort((a, b) => {
|
||||
// 如果a是背景图层,它应该排在后面(最底层)
|
||||
if (a.isBackground) return 1;
|
||||
// 如果b是背景图层,它应该排在后面(最底层)
|
||||
if (b.isBackground) return -1;
|
||||
|
||||
// 如果a是固定图层而b不是固定图层,a应该排在后面(固定图层在普通图层下方)
|
||||
if (a.isFixed && !b.isFixed) return 1;
|
||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||
if (b.isFixed && !a.isFixed) return -1;
|
||||
|
||||
// 其他情况保持原有顺序
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层在排序后的正确插入位置
|
||||
* @param {Object} newLayer 新图层
|
||||
* @param {string} targetLayerId 目标图层ID(插入到该图层之前)
|
||||
* @returns {number} 插入位置索引
|
||||
*/
|
||||
getInsertIndex(newLayer, targetLayerId = null) {
|
||||
if (!targetLayerId) {
|
||||
// 如果没有指定目标图层,根据图层类型决定插入位置
|
||||
if (newLayer.isBackground) {
|
||||
return this.layers.value.length; // 背景图层插入到最后
|
||||
} else if (newLayer.isFixed) {
|
||||
// 固定图层插入到背景图层之前
|
||||
const bgIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.isBackground
|
||||
);
|
||||
return bgIndex !== -1 ? bgIndex : this.layers.value.length;
|
||||
} else {
|
||||
// 普通图层插入到固定图层之前
|
||||
const fixedIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.isFixed
|
||||
);
|
||||
return fixedIndex !== -1 ? fixedIndex : this.layers.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果指定了目标图层,插入到目标图层之前
|
||||
const targetIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.id === targetLayerId
|
||||
);
|
||||
return targetIndex !== -1 ? targetIndex : this.layers.value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动图层到指定位置
|
||||
* @param {string} layerId 要移动的图层ID
|
||||
* @param {number} newIndex 新位置索引
|
||||
* @returns {boolean} 是否移动成功
|
||||
*/
|
||||
async moveLayerToIndex({ parentId, oldIndex, newIndex, layerId }) {
|
||||
// 检查父图层是否存在
|
||||
// const parentLayer = this.getLayerById(parentId);
|
||||
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (!parentLayer) {
|
||||
console.warn(`父图层 ${parentId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有子图层
|
||||
const childLayers = parentLayer?.children || [];
|
||||
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= childLayers.length ||
|
||||
newIndex >= childLayers.length
|
||||
) {
|
||||
console.warn("子图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证图层ID
|
||||
const layer = childLayers[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("子图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新父图层的children数组 - 执行命令
|
||||
const command = new ReorderChildLayersCommand({
|
||||
parentId,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
layerId,
|
||||
layers: this.layers,
|
||||
canvas: this.canvas,
|
||||
layerSort: this, // 传入当前实例
|
||||
});
|
||||
|
||||
if (this.commandManager) {
|
||||
await this.commandManager?.execute(command);
|
||||
} else {
|
||||
await command?.execute?.();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的有效移动范围
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Object} 包含最小和最大索引的对象
|
||||
*/
|
||||
getLayerMoveRange(layerId) {
|
||||
const layer = this.getLayerById(layerId);
|
||||
if (!layer || layer.isBackground || layer.isFixed) {
|
||||
return { minIndex: -1, maxIndex: -1 };
|
||||
}
|
||||
|
||||
// 普通图层只能在普通图层范围内移动
|
||||
const normalLayers = this.layers.value
|
||||
.map((layer, index) => ({ layer, index }))
|
||||
.filter((item) => !item.layer.isBackground && !item.layer.isFixed);
|
||||
|
||||
if (normalLayers.length === 0) {
|
||||
return { minIndex: -1, maxIndex: -1 };
|
||||
}
|
||||
|
||||
return {
|
||||
minIndex: normalLayers[0].index,
|
||||
maxIndex: normalLayers[normalLayers.length - 1].index,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async reorderLayers(oldIndex, newIndex, layerId) {
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= this.layers.value.length ||
|
||||
newIndex >= this.layers.value.length
|
||||
) {
|
||||
console.warn("图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取要移动的图层
|
||||
const layer = this.layers.value[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是背景层或固定层(不允许排序)
|
||||
if (layer.isBackground || layer.isFixed) {
|
||||
console.warn("背景层和固定层不能参与排序");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查目标位置是否合法(不能移到背景层或固定层的位置)
|
||||
const targetLayer = this.layers.value[newIndex];
|
||||
if (targetLayer && (targetLayer.isBackground || targetLayer.isFixed)) {
|
||||
console.warn("不能移动到背景层或固定层的位置");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行排序
|
||||
const layersArray = [...this.layers.value];
|
||||
const [movedLayer] = layersArray.splice(oldIndex, 1);
|
||||
layersArray.splice(newIndex, 0, movedLayer);
|
||||
|
||||
this.layers.value = layersArray;
|
||||
|
||||
// 重新排列画布对象
|
||||
await this.rearrangeObjects();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子图层排序
|
||||
* @param {string} parentId 父图层ID
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 子图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async reorderChildLayers(parentId, oldIndex, newIndex, layerId) {
|
||||
// 检查父图层是否存在
|
||||
// const parentLayer = this.getLayerById(parentId);
|
||||
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (!parentLayer) {
|
||||
console.warn(`父图层 ${parentId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有子图层
|
||||
const childLayers = parentLayer?.children || [];
|
||||
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= childLayers.length ||
|
||||
newIndex >= childLayers.length
|
||||
) {
|
||||
console.warn("子图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证图层ID
|
||||
const layer = childLayers[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("子图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新父图层的children数组 - 执行命令
|
||||
const command = ReorderChildLayersCommand({
|
||||
parentId,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
layerId,
|
||||
layers: this.layers,
|
||||
canvas: this.canvas,
|
||||
layerSort: this, // 传入当前实例
|
||||
});
|
||||
|
||||
if (this.commandManager) {
|
||||
await this.commandManager?.execute(command);
|
||||
} else {
|
||||
await command?.execute?.();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能排序 - 根据对象类型和位置自动调整图层顺序
|
||||
* @param {Array} targetLayerIds 要排序的图层ID数组
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async smartSort(targetLayerIds = null) {
|
||||
const layersToSort = targetLayerIds
|
||||
? this.layers.value.filter((layer) => targetLayerIds.includes(layer.id))
|
||||
: this.layers.value.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
if (layersToSort.length <= 1) return true;
|
||||
|
||||
// 按照对象类型和位置进行智能排序
|
||||
layersToSort.sort((a, b) => {
|
||||
const aWeight = this.getLayerSortWeight(a);
|
||||
const bWeight = this.getLayerSortWeight(b);
|
||||
|
||||
if (aWeight !== bWeight) {
|
||||
return bWeight - aWeight; // 权重高的在上层
|
||||
}
|
||||
|
||||
// 权重相同时,按照Y坐标排序(Y值小的在上层)
|
||||
const aY = this.getLayerAverageY(a);
|
||||
const bY = this.getLayerAverageY(b);
|
||||
|
||||
return aY - bY;
|
||||
});
|
||||
|
||||
// 更新图层顺序
|
||||
const sortedLayerIds = layersToSort.map((layer) => layer.id);
|
||||
const otherLayers = this.layers.value.filter(
|
||||
(layer) => !sortedLayerIds.includes(layer.id)
|
||||
);
|
||||
|
||||
// 重新组织图层数组:保持背景层和固定层的位置
|
||||
const newLayers = [];
|
||||
|
||||
// 添加普通图层(已排序)
|
||||
newLayers.push(...layersToSort);
|
||||
|
||||
// 添加其他普通图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (!layer.isBackground && !layer.isFixed) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加固定图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (layer.isFixed) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加背景图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (layer.isBackground) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
this.layers.value = newLayers;
|
||||
await this.rearrangeObjects();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的排序权重
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 排序权重
|
||||
*/
|
||||
getLayerSortWeight(layer) {
|
||||
const weightMap = LayerSortConstants.LAYER_PRIORITY;
|
||||
|
||||
if (layer.isBackground) return weightMap[LayerType.BACKGROUND];
|
||||
if (layer.isFixed) return weightMap[LayerType.FIXED];
|
||||
if (layer.children?.length > 0) return weightMap[LayerType.GROUP];
|
||||
|
||||
// 根据对象类型调整权重
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
const firstObj = layer.fabricObjects[0];
|
||||
if (firstObj.type === "text") return weightMap[LayerType.NORMAL] + 10;
|
||||
if (firstObj.type === "image") return weightMap[LayerType.NORMAL] + 5;
|
||||
}
|
||||
|
||||
return weightMap[LayerType.NORMAL];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层对象的平均Y坐标
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 平均Y坐标
|
||||
*/
|
||||
getLayerAverageY(layer) {
|
||||
let totalY = 0;
|
||||
let count = 0;
|
||||
|
||||
if (layer.fabricObject) {
|
||||
totalY += layer.fabricObject.top || 0;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
totalY += obj.top || 0;
|
||||
count++;
|
||||
});
|
||||
}
|
||||
|
||||
return count > 0 ? totalY / count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化图层结构 - 清理空图层、合并相邻相似图层等
|
||||
* @returns {Object} 优化结果统计
|
||||
*/
|
||||
async optimizeLayerStructure() {
|
||||
const stats = {
|
||||
removedEmptyLayers: 0,
|
||||
mergedLayers: 0,
|
||||
reorderedLayers: 0,
|
||||
};
|
||||
|
||||
// 清理空图层
|
||||
const emptyLayers = this.layers.value.filter(
|
||||
(layer) =>
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
(!layer.fabricObjects || layer.fabricObjects.length === 0) &&
|
||||
(!layer.children || layer.children.length === 0)
|
||||
);
|
||||
|
||||
emptyLayers.forEach((layer) => {
|
||||
const index = this.layers.value.findIndex((l) => l.id === layer.id);
|
||||
if (index !== -1) {
|
||||
this.layers.value.splice(index, 1);
|
||||
stats.removedEmptyLayers++;
|
||||
}
|
||||
});
|
||||
|
||||
// 重新排序以确保正确的层级关系
|
||||
const wasReordered = this.sortLayers();
|
||||
if (wasReordered) {
|
||||
stats.reorderedLayers = this.layers.value.length;
|
||||
await this.rearrangeObjects();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图层排序工具实例
|
||||
* @param {Object} canvas fabric.js画布实例
|
||||
* @param {Object} layers 图层数组响应式引用
|
||||
* @returns {LayerSort} 图层排序工具实例
|
||||
*/
|
||||
export const createLayerSort = (canvas, layers, options = {}) => {
|
||||
return new LayerSort(canvas, layers, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序相关的常量
|
||||
*/
|
||||
export const LayerSortConstants = {
|
||||
// 图层类型优先级(数值越大优先级越高,在画布中越靠上)
|
||||
LAYER_PRIORITY: {
|
||||
[LayerType.BACKGROUND]: 0,
|
||||
[LayerType.FIXED]: 1,
|
||||
[LayerType.NORMAL]: 2,
|
||||
[LayerType.GROUP]: 2,
|
||||
},
|
||||
|
||||
// 批处理大小
|
||||
BATCH_SIZE: 50,
|
||||
|
||||
// 性能阈值
|
||||
PERFORMANCE_THRESHOLD: {
|
||||
OBJECTS_COUNT: 100, // 超过此数量使用异步处理
|
||||
LAYERS_COUNT: 20, // 超过此数量使用分批处理
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序辅助函数
|
||||
*/
|
||||
export const LayerSortUtils = {
|
||||
/**
|
||||
* 检查是否需要异步处理
|
||||
* @param {number} objectsCount 对象数量
|
||||
* @param {number} layersCount 图层数量
|
||||
* @returns {boolean} 是否需要异步处理
|
||||
*/
|
||||
shouldUseAsyncProcessing(objectsCount, layersCount) {
|
||||
return (
|
||||
objectsCount > LayerSortConstants.PERFORMANCE_THRESHOLD.OBJECTS_COUNT ||
|
||||
layersCount > LayerSortConstants.PERFORMANCE_THRESHOLD.LAYERS_COUNT
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取图层的排序权重
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 排序权重
|
||||
*/
|
||||
getLayerSortWeight(layer) {
|
||||
if (layer.isBackground)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.BACKGROUND];
|
||||
if (layer.isFixed)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.FIXED];
|
||||
if (layer.children?.length > 0)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.GROUP];
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.NORMAL];
|
||||
},
|
||||
|
||||
/**
|
||||
* 比较两个图层的排序优先级
|
||||
* @param {Object} layerA 图层A
|
||||
* @param {Object} layerB 图层B
|
||||
* @returns {number} 比较结果
|
||||
*/
|
||||
compareLayerPriority(layerA, layerB) {
|
||||
const weightA = this.getLayerSortWeight(layerA);
|
||||
const weightB = this.getLayerSortWeight(layerB);
|
||||
return weightB - weightA; // 权重高的排在前面(画布上层)
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序混入方法 - 用于在 LayerManager 中使用
|
||||
*/
|
||||
export const LayerSortMixin = {
|
||||
/**
|
||||
* 初始化图层排序工具
|
||||
*/
|
||||
initLayerSort() {
|
||||
this.layerSort = new LayerSort(this.canvas, this.layers);
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新排列画布对象
|
||||
*/
|
||||
rearrangeCanvasObjects() {
|
||||
if (this.layerSort) {
|
||||
// 检查是否需要异步处理
|
||||
const objectsCount = this.canvas?.getObjects()?.length || 0;
|
||||
const layersCount = this.layers?.value?.length || 0;
|
||||
|
||||
if (LayerSortUtils.shouldUseAsyncProcessing(objectsCount, layersCount)) {
|
||||
return this.layerSort.rearrangeObjectsAsync();
|
||||
} else {
|
||||
this.layerSort.rearrangeObjects();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 排序图层
|
||||
*/
|
||||
sortLayersWithTool() {
|
||||
if (this.layerSort) {
|
||||
this.layers.value = this.layerSort.sortLayers();
|
||||
return this.layerSort.rearrangeObjects();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 智能排序图层
|
||||
*/
|
||||
smartSortLayers(targetLayerIds = null) {
|
||||
if (this.layerSort) {
|
||||
return this.layerSort.smartSort(targetLayerIds);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化图层结构
|
||||
*/
|
||||
optimizeLayerStructure() {
|
||||
if (this.layerSort) {
|
||||
return this.layerSort.optimizeLayerStructure();
|
||||
}
|
||||
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { canvasConfig } from "../config/canvasConfig";
|
||||
|
||||
/**
|
||||
|
||||
@@ -434,25 +434,324 @@ export function createCancellablePromise(executor) {
|
||||
};
|
||||
}
|
||||
|
||||
// 导出所有工具函数
|
||||
export default {
|
||||
deepCompare,
|
||||
deepClone,
|
||||
applyDiff,
|
||||
throttle,
|
||||
debounce,
|
||||
generateId,
|
||||
formatFileSize,
|
||||
formatDuration,
|
||||
isValidCommand,
|
||||
isPromise,
|
||||
safeJSONParse,
|
||||
safeJSONStringify,
|
||||
getObjectDepth,
|
||||
getObjectSize,
|
||||
checkBrowserSupport,
|
||||
delay,
|
||||
retry,
|
||||
batchProcess,
|
||||
createCancellablePromise,
|
||||
/**
|
||||
* 增强版检查对象是否在画布中(包括组内对象)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {Object} { flag: boolean, object: fabric.Object, parent: fabric.Group|null }
|
||||
*/
|
||||
export function objectIsInCanvas(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
const targetId = targetObj.id;
|
||||
if (!targetId) {
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
// 首先检查顶层对象
|
||||
const topLevelObjects = canvas.getObjects();
|
||||
|
||||
// 直接在顶层查找
|
||||
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
|
||||
if (directMatch) {
|
||||
return { flag: true, object: directMatch, parent: null };
|
||||
}
|
||||
|
||||
// 递归检查组内对象
|
||||
for (const obj of topLevelObjects) {
|
||||
if (obj.type === "group") {
|
||||
const result = findObjectInGroup(obj, targetId);
|
||||
if (result.found) {
|
||||
return { flag: true, object: result.object, parent: obj };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 在组中递归查找对象
|
||||
* @param {fabric.Group} group 组对象
|
||||
* @param {string} targetId 目标对象ID
|
||||
* @returns {Object} { found: boolean, object: fabric.Object|null }
|
||||
*/
|
||||
function findObjectInGroup(group, targetId) {
|
||||
if (!group || group.type !== "group" || !group.getObjects) {
|
||||
return { found: false, object: null };
|
||||
}
|
||||
|
||||
const groupObjects = group.getObjects();
|
||||
|
||||
for (const obj of groupObjects) {
|
||||
if (obj.id === targetId) {
|
||||
return { found: true, object: obj };
|
||||
}
|
||||
|
||||
// 递归检查嵌套组
|
||||
if (obj.type === "group") {
|
||||
const nestedResult = findObjectInGroup(obj, targetId);
|
||||
if (nestedResult.found) {
|
||||
return nestedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false, object: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ID查找对象(增强版)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {string} objectId 对象ID
|
||||
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
|
||||
*/
|
||||
export function findObjectById(canvas, objectId) {
|
||||
if (!canvas || !objectId) {
|
||||
return { object: null, parent: null };
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, { id: objectId });
|
||||
return { object: result.object, parent: result.parent };
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全移除画布对象(包括组内对象)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {boolean} 是否成功移除
|
||||
*/
|
||||
export function removeCanvasObjectByObject(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, targetObj);
|
||||
|
||||
if (!result.flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.parent) {
|
||||
// 对象在组中,从组中移除
|
||||
result.parent.removeWithUpdate(result.object);
|
||||
} else {
|
||||
// 对象在顶层,直接从画布移除
|
||||
canvas.remove(result.object);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("移除对象时发生错误:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化画布渲染 统一渲染完成后才执行
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {Function} callback 渲染执行函数
|
||||
*/
|
||||
export const optimizeCanvasRendering = async (canvas, callback) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!canvas || typeof callback !== "function") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 暂停渲染以提高性能
|
||||
canvas.skipTargetFind = true;
|
||||
// 开始渲染
|
||||
// 暂停实时渲染和对象查找
|
||||
const wasRenderOnAddRemove = canvas.renderOnAddRemove;
|
||||
canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||||
|
||||
// 等待下一帧渲染完成
|
||||
requestAnimationFrame(async () => {
|
||||
// 恢复渲染设置
|
||||
canvas.skipTargetFind = false;
|
||||
canvas.renderOnAddRemove = wasRenderOnAddRemove;
|
||||
if (isPromise(callback)) {
|
||||
await callback?.();
|
||||
} else {
|
||||
callback?.();
|
||||
}
|
||||
canvas.renderAll(); // 确保画布重新渲染 - 同步渲染
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取对象在画布中的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {number} z-index位置,-1表示未找到
|
||||
*/
|
||||
export function getObjectZIndex(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, targetObj);
|
||||
if (!result.flag) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (result.parent) {
|
||||
// 对象在组中,返回组在画布中的位置
|
||||
const allObjects = canvas.getObjects();
|
||||
return allObjects.indexOf(result.parent);
|
||||
} else {
|
||||
// 对象在顶层,直接返回在画布中的位置
|
||||
const allObjects = canvas.getObjects();
|
||||
return allObjects.indexOf(result.object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定的z-index位置插入对象
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} object 要插入的对象
|
||||
* @param {number} zIndex z-index位置
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功插入
|
||||
*/
|
||||
export function insertObjectAtZIndex(canvas, object, zIndex, renderAll = true) {
|
||||
if (!canvas || !object || zIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保z-index不超过当前对象数量
|
||||
const maxIndex = canvas.getObjects().length;
|
||||
const safeZIndex = Math.min(zIndex, maxIndex);
|
||||
|
||||
canvas.insertAt(object, safeZIndex, false);
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("插入对象到指定z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动对象到指定的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} object 要移动的对象
|
||||
* @param {number} zIndex 目标z-index位置
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功移动
|
||||
*/
|
||||
export function moveObjectToZIndex(canvas, object, zIndex, renderAll = true) {
|
||||
if (!canvas || !object || zIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, object);
|
||||
if (!result.flag) {
|
||||
console.warn("对象不在画布中,无法移动");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保z-index不超过当前对象数量-1(因为当前对象也在其中)
|
||||
const maxIndex = canvas.getObjects().length - 1;
|
||||
const safeZIndex = Math.min(zIndex, maxIndex);
|
||||
|
||||
result.object.moveTo(safeZIndex);
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("移动对象到指定z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换两个对象的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} obj1 第一个对象
|
||||
* @param {fabric.Object} obj2 第二个对象
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功交换
|
||||
*/
|
||||
export function swapObjectsZIndex(canvas, obj1, obj2, renderAll = true) {
|
||||
if (!canvas || !obj1 || !obj2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const zIndex1 = getObjectZIndex(canvas, obj1);
|
||||
const zIndex2 = getObjectZIndex(canvas, obj2);
|
||||
|
||||
if (zIndex1 === -1 || zIndex2 === -1) {
|
||||
console.warn("其中一个或两个对象不在画布中");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先移动z-index较大的对象,避免索引冲突
|
||||
if (zIndex1 > zIndex2) {
|
||||
moveObjectToZIndex(canvas, obj2, zIndex1, false);
|
||||
moveObjectToZIndex(canvas, obj1, zIndex2, false);
|
||||
} else {
|
||||
moveObjectToZIndex(canvas, obj1, zIndex2, false);
|
||||
moveObjectToZIndex(canvas, obj2, zIndex1, false);
|
||||
}
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("交换对象z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布中所有对象的z-index信息
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @returns {Array} 包含对象ID和z-index的数组
|
||||
*/
|
||||
export function getAllObjectsZIndex(canvas) {
|
||||
if (!canvas) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objects = canvas.getObjects();
|
||||
return objects.map((obj, index) => ({
|
||||
id: obj.id || `unnamed_${index}`,
|
||||
zIndex: index,
|
||||
type: obj.type,
|
||||
layerId: obj.layerId,
|
||||
object: obj,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层ID获取对象的z-index信息
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Array} 属于指定图层的对象z-index信息
|
||||
*/
|
||||
export function getLayerObjectsZIndex(canvas, layerId) {
|
||||
if (!canvas || !layerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allInfo = getAllObjectsZIndex(canvas);
|
||||
return allInfo.filter((info) => info.layerId === layerId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
|
||||
// 导入新的复合命令
|
||||
import { CreateImageLayerCommand } from "../commands/LayerCommands";
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ChangeFixedImageCommand,
|
||||
AddImageToLayerCommand,
|
||||
} from "../commands/LayerCommands";
|
||||
import { generateId } from "./helper";
|
||||
|
||||
/**
|
||||
* 加载并处理图片
|
||||
@@ -44,6 +45,7 @@ export function loadImage(imageSource, options = {}) {
|
||||
// 设置图片位置 - 默认居中
|
||||
if (options.centerOnCanvas !== false) {
|
||||
fabricImage.set({
|
||||
id: generateId("fabricImage"),
|
||||
left: (options.canvasWidth || 800) / 2,
|
||||
top: (options.canvasHeight || 600) / 2,
|
||||
originX: "center",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LayerType = {
|
||||
VIDEO: "video", // 视频图层 (预留)
|
||||
AUDIO: "audio", // 音频图层 (预留)
|
||||
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
|
||||
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -461,3 +462,161 @@ export function cloneLayer(layer) {
|
||||
|
||||
return clonedLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找图层(包括子图层)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @param {Object} parent 父图层(可选,用于内部递归)
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerRecursively(layers, layerId, parent = null) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在子图层中递归查找
|
||||
* @param {Array} children 子图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @param {Object} parent 父图层
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findInChildLayers(children, layerId, parent) {
|
||||
if (!children || !Array.isArray(children) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (child && child.id === layerId) {
|
||||
return { layer: child, parent };
|
||||
}
|
||||
|
||||
// 如果子图层也是组,继续递归查找
|
||||
if (
|
||||
child &&
|
||||
(child.type === "group" || child.type === LayerType.GROUP) &&
|
||||
child.children &&
|
||||
Array.isArray(child.children)
|
||||
) {
|
||||
const result = findInChildLayers(child.children, layerId, child);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单查找图层(仅在顶级图层中查找,不递归子图层)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @returns {Object|null} 找到的图层对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayer(layers, layerId) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return layers.find((layer) => layer && layer.id === layerId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图层名称查找图层
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerName 要查找的图层名称
|
||||
* @param {boolean} recursive 是否递归查找子图层,默认false
|
||||
* @returns {Object|null} 找到的图层对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerByName(layers, layerName, recursive = false) {
|
||||
if (!layers || !Array.isArray(layers) || !layerName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.name === layerName) {
|
||||
return layer;
|
||||
}
|
||||
|
||||
// 如果需要递归查找且是组图层
|
||||
if (
|
||||
recursive &&
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const found = findLayerByName(layer.children, layerName, true);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的完整路径(包含父图层信息)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @returns {Array} 图层路径数组,从根图层到目标图层
|
||||
*/
|
||||
export function getLayerPath(layers, layerId) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function findPath(currentLayers, targetId, currentPath = []) {
|
||||
for (const layer of currentLayers) {
|
||||
if (!layer) continue;
|
||||
|
||||
const newPath = [...currentPath, layer];
|
||||
|
||||
if (layer.id === targetId) {
|
||||
return newPath;
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找
|
||||
if (
|
||||
layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children))
|
||||
) {
|
||||
const foundPath = findPath(layer.children, targetId, newPath);
|
||||
if (foundPath.length > 0) {
|
||||
return foundPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return findPath(layers, layerId);
|
||||
}
|
||||
|
||||
204
src/component/Canvas/CanvasEditor/utils/layerUtils.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { isArray } from "lodash-es";
|
||||
|
||||
/**
|
||||
* 图层关联工具类
|
||||
* 提供图层与画布对象关联管理的通用方法
|
||||
*/
|
||||
|
||||
/**
|
||||
* 构建单个图层与画布对象的关联关系
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
*/
|
||||
export function buildLayerAssociations(layer, canvasObjects) {
|
||||
if (!layer || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
// 处理单个fabricObject关联
|
||||
if (layer.fabricObject) {
|
||||
// 如果图层已经有关联的fabricObject,确保它的layerId和layerName正确
|
||||
layer.fabricObject =
|
||||
canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
|
||||
}
|
||||
|
||||
if (layer.clippingMask) {
|
||||
// clippingMask 可能是一个fabricObject或组
|
||||
layer.clippingMask =
|
||||
canvasObjects.find((obj) => obj.id === layer.clippingMask.id) || null;
|
||||
}
|
||||
|
||||
// 处理多个fabricObjects关联
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
// 确保每个fabricObject的layerId和layerName正确
|
||||
const obj = canvasObjects.find((obj) => obj.id === fabricObject.id);
|
||||
if (obj) {
|
||||
obj.layerId = layer.id; // 确保对象的layerId正确
|
||||
obj.layerName = layer.name; // 确保对象的layerName正确
|
||||
return obj;
|
||||
}
|
||||
return null; // 如果没有找到对象,返回null
|
||||
})
|
||||
.filter((obj) => obj !== null); // 过滤掉null值
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象与图层的关联关系
|
||||
* @param {Object} layerManager 图层管理器实例
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
*/
|
||||
export function restoreObjectLayerAssociations(layers, canvasObjects) {
|
||||
if (!layers || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
layers.forEach((layer) => {
|
||||
buildLayerAssociations(layer, canvasObjects);
|
||||
// 处理子图层
|
||||
if (layer?.children?.length) {
|
||||
restoreObjectLayerAssociations(layer.children, canvasObjects);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为画布对象设置图层信息
|
||||
* @param {Object} fabricObject 画布对象
|
||||
* @param {Object} layer 图层对象
|
||||
*/
|
||||
export function setObjectLayerInfo(fabricObject, layer) {
|
||||
if (!fabricObject || !layer) return;
|
||||
|
||||
fabricObject.layerId = layer.id;
|
||||
fabricObject.layerName = layer.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除画布对象的图层信息
|
||||
* @param {Object} fabricObject 画布对象
|
||||
*/
|
||||
export function clearObjectLayerInfo(fabricObject) {
|
||||
if (!fabricObject) return;
|
||||
|
||||
delete fabricObject.layerId;
|
||||
delete fabricObject.layerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图层关联关系的完整性
|
||||
* @param {Object} layerManager 图层管理器实例
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @returns {Object} 验证结果 { valid: boolean, issues: Array }
|
||||
*/
|
||||
export function validateLayerAssociations(layers, canvasObjects) {
|
||||
const issues = [];
|
||||
|
||||
// 检查画布对象是否都有对应的图层
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.layerId) {
|
||||
const layer = layers.find((l) => l.id === obj.layerId);
|
||||
if (!layer) {
|
||||
issues.push({
|
||||
type: "orphaned_object",
|
||||
objectId: obj.id,
|
||||
layerId: obj.layerId,
|
||||
message: `对象 ${obj.id} 关联的图层 ${obj.layerId} 不存在`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
issues.push({
|
||||
type: "missing_layer_id",
|
||||
objectId: obj.id,
|
||||
message: `对象 ${obj.id} 缺少图层ID关联`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查图层是否都有对应的画布对象
|
||||
layers.forEach((layer) => {
|
||||
if (layer.fabricObject && layer.fabricObject.id) {
|
||||
const obj = canvasObjects.find((o) => o.id === layer.fabricObject.id);
|
||||
if (!obj) {
|
||||
issues.push({
|
||||
type: "missing_object",
|
||||
layerId: layer.id,
|
||||
objectId: layer.fabricObject.id,
|
||||
message: `图层 ${layer.id} 关联的对象 ${layer.fabricObject.id} 不存在于画布中`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects.forEach((fabricObj) => {
|
||||
if (fabricObj.id) {
|
||||
const obj = canvasObjects.find((o) => o.id === fabricObj.id);
|
||||
if (!obj) {
|
||||
issues.push({
|
||||
type: "missing_object",
|
||||
layerId: layer.id,
|
||||
objectId: fabricObj.id,
|
||||
message: `图层 ${layer.id} 关联的对象 ${fabricObj.id} 不存在于画布中`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: issues.length === 0,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化layers对象属性,只保留必要的属性
|
||||
* @param {Array} layers 图层数组 simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
|
||||
*/
|
||||
|
||||
export function simplifyLayers(layers) {
|
||||
if (!layers || !isArray(layers)) {
|
||||
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
||||
return [];
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
// 处理图层遮罩
|
||||
// 如果clippingMask是一个fabricObject或组,确保它的id正确 // 因为是fabric对象,所以没办法直接获取id,只能通过序列化获取
|
||||
if (layer.clippingMask) {
|
||||
layer.clippingMask = layer.clippingMask?.id || null;
|
||||
}
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
layer.fabricObject = layer.fabricObject?.id;
|
||||
}
|
||||
// 处理多个fabricObjects
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
return fabricObject?.id || null; // 确保每个fabricObject都能转换为对象
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
// 处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
layer.children = simplifyLayers(layer.children);
|
||||
}
|
||||
|
||||
// 只保留必要的属性
|
||||
layer = {
|
||||
id: layer.id,
|
||||
name: layer.name,
|
||||
visible: layer.visible,
|
||||
locked: layer.locked,
|
||||
opacity: layer.opacity,
|
||||
clippingMask: layer.clippingMask || null,
|
||||
fabricObject: layer.fabricObject || null,
|
||||
fabricObjects: layer.fabricObjects || [],
|
||||
children: layer.children || [],
|
||||
isBackground: layer.isBackground || false,
|
||||
ifFixed: layer.ifFixed || false,
|
||||
};
|
||||
});
|
||||
|
||||
return layers;
|
||||
}
|
||||
@@ -109,7 +109,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted} from 'vue'
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted, watch} from 'vue'
|
||||
import { Https } from "@/tool/https";
|
||||
import { useStore } from "vuex";
|
||||
import sketchCategory from "@/component/HomePage/sketchCategory.vue";
|
||||
@@ -165,8 +165,14 @@ export default defineComponent({
|
||||
mannequinStyle:computed(()=>store.state.UserHabit.mannequinStyle),//风格列表
|
||||
sexList:computed(()=>store.state.UserHabit.sex.value),//风格列表
|
||||
ageGroupList:computed(()=>store.state.UserHabit.ageGroup),//风格列表
|
||||
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
|
||||
|
||||
})
|
||||
watch(()=>detailData.selectObject,(newValue,oldValue)=>{
|
||||
detailData.mannequinData.sex = newValue.sex?newValue.sex:'Female'
|
||||
detailData.mannequinData.style = newValue.style?newValue.style:''
|
||||
detailData.mannequinData.ageGroup = newValue.ageGroup?newValue.ageGroup:''
|
||||
},{immediate:true})
|
||||
const getDetailListData = reactive({
|
||||
total:0,
|
||||
pageSize:10,
|
||||
|
||||
@@ -71,6 +71,7 @@ export default defineComponent({
|
||||
selectItem.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
||||
},{immediate: true,})
|
||||
watch(()=>detailData.frontBack?.body?.path,(newVal)=>{
|
||||
|
||||
let sacle = 0
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
@@ -82,16 +83,22 @@ export default defineComponent({
|
||||
let value = item.style[key]
|
||||
if(typeof value !== 'number'){
|
||||
value = value.replace('px','')
|
||||
item.style[key] = value
|
||||
}else{
|
||||
item.style[key] = value*sacle+'px'
|
||||
}
|
||||
item.style[key] = value*sacle+'px'
|
||||
// item.style[key] = value*sacle+'px'
|
||||
}
|
||||
for (const key in detailData.frontBack.back[index].style) {
|
||||
if(key == 'zIndex')return
|
||||
let value = detailData.frontBack.back[index].style[key]
|
||||
if(typeof value !== 'number'){
|
||||
value = value.replace('px','')
|
||||
detailData.frontBack.back[index].style[key] = value
|
||||
}else{
|
||||
detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
}
|
||||
detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
// detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -415,7 +422,6 @@ export default defineComponent({
|
||||
}
|
||||
for (const key in data.instance.frontBack.back[index].style) {
|
||||
if(key == 'zIndex')return
|
||||
console.log(data.instance.frontBack.back[index].style[key].replace(/px/g,''))
|
||||
data.instance.frontBack.back[index].style[key] = data.instance.frontBack.back[index].style[key].replace(/px/g,'')*sacle+'px'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -39,16 +39,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <input
|
||||
class="search_input"
|
||||
@input="ifMaximumLength"
|
||||
:placeholder="(scene?.value == 'Slogan' && type_.type2 == 'Printboard')?isSloganHint:$t('Generate.inputContent1')"
|
||||
:maxlength='inputShow?0:9999'
|
||||
v-model="searchPictureName"
|
||||
@keydown.enter="getgenerate()"
|
||||
@click="inputFocus()"
|
||||
@paste="onPaste"
|
||||
/> -->
|
||||
<textarea
|
||||
class="textarea"
|
||||
@input="ifMaximumLength"
|
||||
@@ -82,7 +72,7 @@
|
||||
<!-- <i v-show="!isTextarea" class="fi fi-br-expand" @click.stop="setTextareaShow"></i>
|
||||
<i v-show="isTextarea" class="fi fi-bs-compress" @click.stop="setTextareaShow"></i> -->
|
||||
</div>
|
||||
<div class="input_box_btnBox sketch" v-else>
|
||||
<div class="input_box_btnBox sketch" v-else >
|
||||
<div class="upload_item" v-show="sketchboardList.length > 0">
|
||||
<div
|
||||
class="upload_file_item"
|
||||
@@ -125,9 +115,9 @@
|
||||
>
|
||||
</a-upload>
|
||||
</i>
|
||||
<div :title="$t('Generate.style')">
|
||||
<!-- <div :title="$t('Generate.style')">
|
||||
<generalMenu :dataList="printModelList" :isCanvas="type_.type2 == 'Sketchboard'" @setprintModel="setprintModel" :item="printModel"></generalMenu>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<textarea
|
||||
v-show="isTextarea"
|
||||
@@ -141,18 +131,19 @@
|
||||
<div class="generage_btn_box">
|
||||
<div class="generage_btn started_btn" v-show="!isGenerate">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getgenerate()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="
|
||||
type_.type2 == 'Moodboard' ||
|
||||
(type_.type2 == 'Printboard' && scene?.value == 'Pattern') ||
|
||||
(type_.type2 == 'Sketchboard' && scene?.value == 'generate')" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
<div class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState && scene?.value != 'extract'">
|
||||
<div v-for="item in speedList" v-show="(type_.type2 == 'Moodboard' && item?.value != 'flux') || (type_.type2 == 'Sketchboard' && item?.value != 'flux') || type_.type2 == 'Printboard'" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
|
||||
</div>
|
||||
<div class="content" v-show="speedState && scene?.value == 'extract'">
|
||||
<div v-for="item in extractList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="isGenerate && !remGenerate">
|
||||
<i class="fi fi-br-loading" ></i>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="remGenerate" @click="removeGenerate">
|
||||
<div class="generage_btn started_btn" v-show="remGenerate" @click.stop="removeGenerate">
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
|
||||
@@ -182,19 +173,22 @@
|
||||
@click="generageAdd(item)"
|
||||
:class="[item.status != 'Success'?'hideEvents':'',item?.checked?'active':'']"
|
||||
>
|
||||
<img v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
|
||||
<sketchCategory v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
|
||||
<img v-if="item?.imgUrl" v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
|
||||
<div v-else class="loading">
|
||||
<a-spin size="large" ></a-spin>
|
||||
</div>
|
||||
<sketchCategory v-show="item?.imgUrl" v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
|
||||
<div
|
||||
v-show="item?.imgUrl"
|
||||
class="delete_like_file_block left1"
|
||||
:class="[driver__.driver?'hideEvents':'',]"
|
||||
>
|
||||
<i v-if="!item.like" class="fi fi-rr-heart" @click.stop="likeFile(item,'like')"></i>
|
||||
<i v-if="!item.like" class="fi fi-rr-heart" @click="likeFile(item,'like')"></i>
|
||||
<i v-else class="fi fi-sr-heart" :adminLike="!!item.like" @click.stop="likeFile(item,'noLike')"></i>
|
||||
</div>
|
||||
<div class="delete_like_file_block left" :class="[driver__.driver?'hideEvents':'']">
|
||||
<div v-show="item?.imgUrl" class="delete_like_file_block left">
|
||||
<i class="fi fi-bs-expand-arrows-alt" @click.stop="scaleImage(index)"></i>
|
||||
</div>
|
||||
<div class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
|
||||
<div v-show="item?.imgUrl" class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
|
||||
<span class="icon iconfont icon-shanchu operate_icon"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,6 +288,21 @@ export default defineComponent({
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
value:'flux',
|
||||
},
|
||||
],
|
||||
extractList:[
|
||||
{
|
||||
title:'This method may produce slight discrepancies between the extracted line art and the original image.',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Note: The extracted line art might have minor variations from the original.',
|
||||
label:'FLUX',
|
||||
value:'flux',
|
||||
},
|
||||
],
|
||||
speedState:false,
|
||||
@@ -311,8 +320,16 @@ export default defineComponent({
|
||||
document.removeEventListener('click',openSpeed)
|
||||
}
|
||||
}
|
||||
watch(()=>props.scene,(newVal,oldVal)=>{
|
||||
if(newVal.value == 'extract'){
|
||||
speed.speedData = speed.extractList[0]
|
||||
}else{
|
||||
speed.speedData = speed.speedList[0]
|
||||
}
|
||||
})
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
return {
|
||||
userDetail,
|
||||
@@ -471,6 +488,7 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
generageAdd(data: any) {
|
||||
if(!data?.imgUrl)return
|
||||
data.type_ = this.type_;
|
||||
data.type_.type1 = data.designType?data.designType:this.type_.type1
|
||||
data.resData = JSON.parse(JSON.stringify(data))
|
||||
@@ -487,12 +505,6 @@ export default defineComponent({
|
||||
data.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:maxImg})
|
||||
this.store.commit("addGenerateMaterialFils", data);
|
||||
// console.log(this.fileList);
|
||||
let moodboard = this.store.state.UploadFilesModule.moodboardGenerateFiles
|
||||
let sketch = this.store.state.UploadFilesModule.sketchGenerateFiles
|
||||
let print = this.store.state.UploadFilesModule.printGenerateFiles
|
||||
if((moodboard.length >= 2 || print.length >= 2 || sketch.length >= 2) && this.driver__.driver){
|
||||
driverObj__.moveNext()
|
||||
}
|
||||
},
|
||||
beforeUpload(file: any) {
|
||||
const isJpgOrPng =
|
||||
@@ -543,94 +555,109 @@ export default defineComponent({
|
||||
})
|
||||
},
|
||||
getgenerate(){
|
||||
if(this.scene?.value == 'extract'){
|
||||
this.imageToSketch()
|
||||
return
|
||||
}
|
||||
// if(this.scene?.value == 'extract'){
|
||||
// this.imageToSketch()
|
||||
// return
|
||||
// }
|
||||
this.isTextarea = false
|
||||
this.isInputFocus = false
|
||||
if(this.isGenerate)return
|
||||
clearInterval(this.remGenerateTime)
|
||||
if(this.searchPictureName){
|
||||
let arr = this.searchPictureName.split(/\s+/).length
|
||||
if(arr > 250){
|
||||
message.info(
|
||||
this.t('Generate.jsContent4')
|
||||
);
|
||||
return
|
||||
let httpsUrl = Https.httpUrls.generatePrepare
|
||||
let data
|
||||
if(this.scene?.value == 'extract'){
|
||||
httpsUrl = Https.httpUrls.imageToSketch
|
||||
if((!this.printModel?.id && !this.printModel?.value) || !this.sketchboardList?.[0]?.id)return message.info(this.t('Generate.jsContent4'));
|
||||
data = {
|
||||
"elementId": this.sketchboardList[0].id,
|
||||
gender:this.workspace.sex,
|
||||
"style": this.printModel.value,
|
||||
"styleImageId": this.printModel?.id?this.printModel?.id:'',
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
}
|
||||
}else{
|
||||
if(this.sketchboardList?.[0]?.imgUrl){
|
||||
|
||||
if(this.searchPictureName){
|
||||
let arr = this.searchPictureName.split(/\s+/).length
|
||||
if(arr > 250){
|
||||
message.info(
|
||||
this.t('Generate.jsContent4')
|
||||
);
|
||||
return
|
||||
}
|
||||
}else{
|
||||
message.info(
|
||||
this.t('Generate.jsContent5')
|
||||
);
|
||||
return
|
||||
if(this.sketchboardList?.[0]?.imgUrl){
|
||||
|
||||
}else{
|
||||
message.info(
|
||||
this.t('Generate.jsContent5')
|
||||
);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let level2Type = ''
|
||||
let collectionElementId = ''
|
||||
let base64 = ''
|
||||
if(this.sketchboardList?.[0]){
|
||||
collectionElementId = this.sketchboardList[0].id
|
||||
if(this.sketchboardList[0].base64){
|
||||
base64 = this.sketchboardList[0].imgUrl
|
||||
}
|
||||
}
|
||||
let sloganText = ''
|
||||
sloganText = this.searchPictureName
|
||||
if(this.upload.level1Type == "Sketchboard"){
|
||||
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
|
||||
if(this.workspace.styleName){
|
||||
sloganText = `${this.workspace.styleName},${sloganText}`
|
||||
}
|
||||
}else if(this.upload.level1Type == "Printboard"){
|
||||
level2Type = this.scene?.value
|
||||
if(level2Type == 'Slogan' && this.searchPictureName == ''){
|
||||
sloganText = this.isSloganHint
|
||||
}else if(level2Type == 'Pattern'){
|
||||
sloganText = `${this.printModel.value},${sloganText}`
|
||||
}
|
||||
if(!base64 && level2Type == 'Slogan'){
|
||||
message.info(this.t('Generate.jsContent10'));
|
||||
return
|
||||
}
|
||||
}
|
||||
data = {
|
||||
generateType:'text',
|
||||
designType:'collection',
|
||||
collectionElementId:collectionElementId,
|
||||
level1Type:this.upload.level1Type,
|
||||
level2Type:level2Type,
|
||||
text:sloganText,
|
||||
seed:this.searchPictureSeed,
|
||||
userId:this?.userDetail?.userId,
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
isTestUser:this.driver__.driver?false:this.isTest,
|
||||
gender:this.workspace.sex,
|
||||
sloganBase64:base64,
|
||||
ageGroup:this.workspace.ageGroup
|
||||
}
|
||||
this.generateLevel2Type = data.level2Type
|
||||
}
|
||||
let level2Type = ''
|
||||
let collectionElementId = ''
|
||||
let base64 = ''
|
||||
if(this.sketchboardList?.[0]){
|
||||
collectionElementId = this.sketchboardList[0].id
|
||||
if(this.sketchboardList[0].base64){
|
||||
base64 = this.sketchboardList[0].imgUrl
|
||||
}
|
||||
}
|
||||
let sloganText = ''
|
||||
sloganText = this.searchPictureName
|
||||
if(this.upload.level1Type == "Sketchboard"){
|
||||
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
|
||||
if(this.workspace.styleName){
|
||||
sloganText = `${this.workspace.styleName},${sloganText}`
|
||||
}
|
||||
}else if(this.upload.level1Type == "Printboard"){
|
||||
level2Type = this.scene?.value
|
||||
if(level2Type == 'Slogan' && this.searchPictureName == ''){
|
||||
sloganText = this.isSloganHint
|
||||
}else if(level2Type == 'Pattern'){
|
||||
sloganText = `${this.printModel.value},${sloganText}`
|
||||
}
|
||||
if(!base64 && level2Type == 'Slogan'){
|
||||
message.info(this.t('Generate.jsContent10'));
|
||||
return
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
generateType:'text',
|
||||
designType:'collection',
|
||||
collectionElementId:collectionElementId,
|
||||
level1Type:this.upload.level1Type,
|
||||
level2Type:level2Type,
|
||||
text:sloganText,
|
||||
seed:this.searchPictureSeed,
|
||||
userId:this?.userDetail?.userId,
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
isTestUser:this.driver__.driver?false:this.isTest,
|
||||
gender:this.workspace.sex,
|
||||
sloganBase64:base64,
|
||||
}
|
||||
this.generateLevel2Type = data.level2Type
|
||||
|
||||
|
||||
this.isGenerate = true
|
||||
this.remGenerateTime = setTimeout(()=>{
|
||||
this.remGenerate = true
|
||||
},10000)
|
||||
Https.axiosPost(Https.httpUrls.generatePrepare, data).then(
|
||||
// this.remGenerateTime = setTimeout(()=>{
|
||||
// },10000)
|
||||
Https.axiosPost(httpsUrl, data).then(
|
||||
(rv) => {
|
||||
// if(data.isTestUser){
|
||||
// if(rv.leftUsageCount >= 1){
|
||||
// message.warning(this.t('Generate.jsContent8',{num:rv.leftUsageCount,str:this.t('collectionModal.Moodboard')}));
|
||||
// }else if(rv.leftUsageCount == 0){
|
||||
// message.warning(this.t('Generate.jsContent9',{str:this.t('collectionModal.Moodboard')}));
|
||||
// this.isGenerate = false
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
if(this.scene?.value == 'extract'){
|
||||
rv = {
|
||||
uniqueId:[rv]
|
||||
}
|
||||
}
|
||||
let rvData = rv.uniqueId.map((item:any)=>{
|
||||
return{taskId:item,status:''}
|
||||
})
|
||||
this.remGenerate = true//出现取消按钮
|
||||
this.fileList.unshift(...rvData)
|
||||
this.setGenerate(rv.uniqueId)
|
||||
|
||||
}
|
||||
).catch(res=>{
|
||||
this.generateLevel2Type = ''
|
||||
@@ -676,7 +703,9 @@ export default defineComponent({
|
||||
if(element.status == 'Success'){
|
||||
element.imgUrl = element.url
|
||||
element.id_ = GO.id++
|
||||
this.fileList.unshift(element)
|
||||
let index = this.fileList.findIndex((item:any)=>item.taskId == element.taskId)
|
||||
this.fileList[index] = element
|
||||
// this.fileList.unshift(element)
|
||||
data = data.filter((item:any) => item !== element.taskId);
|
||||
if(this.type_.type2 == 'Sketchboard'){
|
||||
this.sketchCatecoryList.forEach((itemCategory:any) => {
|
||||
@@ -689,6 +718,10 @@ export default defineComponent({
|
||||
element.categoryValue = this.scene?.value
|
||||
element.category = this.scene?.name
|
||||
}
|
||||
}else if(element.status == 'Fail' || element.status == 'Invalid'){
|
||||
data = data.filter((item:any) => item !== element.taskId);
|
||||
this.fileList = this.fileList.filter((item:any) => item.taskId !== element.taskId);
|
||||
message.info(this.t('Generate.everyTimeEffectPoor'));
|
||||
}
|
||||
});
|
||||
if((data.length == 0)|| (rv.filter((item:any)=>item.status == 'Invalid').length ==data.length)){
|
||||
@@ -718,7 +751,7 @@ export default defineComponent({
|
||||
this.remGenerate = false
|
||||
this.generateLevel2Type = ''
|
||||
});
|
||||
},1000)
|
||||
},5000)
|
||||
},
|
||||
removeGenerate(){
|
||||
//取消操作
|
||||
@@ -737,8 +770,12 @@ export default defineComponent({
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
type: type
|
||||
}
|
||||
|
||||
Https.axiosGet(Https.httpUrls.generateStopWaiting, {params:data}).then(
|
||||
(rv) => {
|
||||
this.generateProceedList.forEach((generateProceedListItem:any)=>{
|
||||
this.fileList = this.fileList.filter((item:any) => generateProceedListItem.taskId!== item.taskId);
|
||||
})
|
||||
this.generateProceedList = []
|
||||
}
|
||||
).catch(res=>{
|
||||
@@ -1044,6 +1081,13 @@ export default defineComponent({
|
||||
pointer-events:none;
|
||||
}
|
||||
}
|
||||
.loading{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
img {
|
||||
// width: calc(10rem*1.2);
|
||||
// height: calc(10rem*1.2);
|
||||
|
||||
@@ -534,9 +534,9 @@ export default defineComponent({
|
||||
imageStrength:(100 - imageStrength)/100,
|
||||
}
|
||||
productImgData.isProductimg = true
|
||||
remPrductimgTime = setTimeout(()=>{
|
||||
productImgData.remProductimg = true
|
||||
},10000)
|
||||
// remPrductimgTime = setTimeout(()=>{
|
||||
// productImgData.remProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.toProduct
|
||||
if(productimgMenu.value.value == 'Relight'){
|
||||
url = Https.httpUrls.relight
|
||||
@@ -544,6 +544,7 @@ export default defineComponent({
|
||||
productImgData.isShowMark = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productImgData.remProductimg = true
|
||||
productImgData.isShowMark = false
|
||||
let arr:any = []
|
||||
rv.forEach((item:any)=>{
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
|
||||
</div>
|
||||
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>Selection Function</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
|
||||
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
</div>
|
||||
@@ -45,10 +50,10 @@
|
||||
</a-slider>
|
||||
<input type="number" readonly v-model="productimgSimilarity">
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
@afterChange="setSimilarity"
|
||||
@@ -58,10 +63,10 @@
|
||||
</a-slider> -->
|
||||
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Highlight')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
|
||||
<a-slider class="system_silder"
|
||||
v-model:value="productimgBrightenValue"
|
||||
:tooltipVisible="false"
|
||||
@@ -92,7 +97,22 @@
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="productImg_content_item_generate_btn input_border">
|
||||
<div class="generage_btn_box" style="margin-left: auto;">
|
||||
<div class="generage_btn started_btn" v-show="!productimgIsProductimg">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="productimgIsProductimg && !productimgRemProductimg">
|
||||
<i class="fi fi-br-loading" ></i>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="productimgRemProductimg" @click="removeProductimg">
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="productImg_content_item_generate_btn input_border">
|
||||
<div class="input_box">
|
||||
<div v-show="!productimgIsProductimg" class="generage_btn started_btn" @click.stop="getPrductimg">
|
||||
{{ $t('Generate.Generate') }}
|
||||
@@ -104,7 +124,7 @@
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="scaleImage_content_imgBox" :class="{active:isComparison}">
|
||||
@@ -189,6 +209,55 @@ export default defineComponent({
|
||||
productimgRelightDirection:props.productData.RelightDirection,
|
||||
productimgRelightDirectionList:props.productData.RelightDirectionList,
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
] as any,
|
||||
speedTypeList:{
|
||||
poseTransfer:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
toPorductImg:[
|
||||
{
|
||||
title:'Generate with high quality',
|
||||
label:'High',
|
||||
relightLabel:'Relight',
|
||||
value:'',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
relightLabel:'Edit',
|
||||
value:'flux',
|
||||
},
|
||||
]
|
||||
},
|
||||
speedState:false,
|
||||
speedData:{
|
||||
title:'Generate high-quality images',
|
||||
relightLabel:'Relight',
|
||||
label:'High',
|
||||
value:'',
|
||||
},
|
||||
})
|
||||
const openSpeed = ()=>{
|
||||
speed.speedState = !speed.speedState
|
||||
if(speed.speedState){
|
||||
document.addEventListener('click',openSpeed)
|
||||
}else{
|
||||
document.removeEventListener('click',openSpeed)
|
||||
}
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
let scaleImage: any = ref(false);
|
||||
let isShowMark = ref(false)
|
||||
let loadingShow = ref(false)
|
||||
@@ -224,9 +293,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
productimg.productimgIsProductimg = true
|
||||
remPrductimgTime = setTimeout(()=>{
|
||||
productimg.productimgRemProductimg = true
|
||||
},10000)
|
||||
// remPrductimgTime = setTimeout(()=>{
|
||||
// productimg.productimgRemProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.relight
|
||||
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
|
||||
url = Https.httpUrls.toProduct
|
||||
@@ -234,6 +303,7 @@ export default defineComponent({
|
||||
isShowMark.value = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productimg.productimgRemProductimg = true
|
||||
isShowMark.value = false
|
||||
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
|
||||
let arr:any = []
|
||||
@@ -381,6 +451,9 @@ export default defineComponent({
|
||||
return {
|
||||
t,
|
||||
...toRefs(productimg),
|
||||
...toRefs(speed),
|
||||
openSpeed,
|
||||
setSpeed,
|
||||
scaleImage,
|
||||
isShowMark,
|
||||
loadingShow,
|
||||
@@ -427,6 +500,12 @@ export default defineComponent({
|
||||
this.scaleImageIndex = index
|
||||
if(dialogueIndex)this.robotAssits = dialogueIndex
|
||||
// let scaleImageList = this.store.state.UploadFilesModule.moodboard
|
||||
if(this.scaleImageList[index].resultType == "PoseTransfer"){
|
||||
this.speedList = this.speedTypeList.poseTransfer
|
||||
}else{
|
||||
this.speedList = this.speedTypeList.toPorductImg
|
||||
}
|
||||
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
|
||||
document.addEventListener('keydown',this.setKeydown)
|
||||
},
|
||||
cancelDsign(){
|
||||
|
||||
@@ -81,11 +81,13 @@ export default defineComponent({
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
video{
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: max-content;
|
||||
}
|
||||
.general_video_btn{
|
||||
color: #fff;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal_title_text">
|
||||
<div>Create Cloud Generation Tasks</div>
|
||||
<div>Create Batch Generation Tasks</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_center admin_page">
|
||||
<div class="admin_state_item">
|
||||
@@ -52,12 +52,24 @@
|
||||
placeholder="Please select"
|
||||
:options="objectList"
|
||||
@search="getHistoryProjectList"
|
||||
@change="changeProject"
|
||||
>
|
||||
<template #option="{ value: val, label, icon,updateTime }">
|
||||
<span :title="updateTime.replace('T', ' ')">{{ label }}</span>
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Name <span>*</span></span>
|
||||
<input
|
||||
v-model="porjectName"
|
||||
:placeholder="placeholder"
|
||||
@focus="focus"
|
||||
@blur="blur"
|
||||
type="text"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item" v-show="buildType">
|
||||
<span>Quantity <span>*</span></span>
|
||||
<input
|
||||
@@ -72,14 +84,17 @@
|
||||
<div v-show="buildType == 'TO_PRODUCT_IMAGE'" class="admin_state_item ">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
<div class="sliderAndImput" style="width: 200px">
|
||||
<a-slider class="system_silder"
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
range
|
||||
:step="5"
|
||||
|
||||
>
|
||||
</a-slider>
|
||||
<!-- <input type="number" readonly v-model="similarity"> -->
|
||||
</a-slider> -->
|
||||
<div style="display: flex;">
|
||||
<input type="number" readonly v-model="similarity[0]">
|
||||
<div style="margin: 0 1rem;">-</div>
|
||||
<input type="number" readonly v-model="similarity[1]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="buildType == 'RELIGHT'" class="admin_state_item ">
|
||||
@@ -117,12 +132,18 @@
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="productImg_content_item_imgBox generalScroll upload_item" v-if="buildType && buildType != 'SERIES_DESIGN' && buildType != 'SINGLE_DESIGN'" v-mousewheel>
|
||||
<div class="content_item_imgBox_itemImg" v-for="item in uploadElement" :key="item">
|
||||
<div
|
||||
class="imgBox"
|
||||
@click="()=>item.isChecked = !item.isChecked"
|
||||
>
|
||||
<img :class="[item?.isChecked?'active':'']" :src="item?.url" class="upload_img"/>
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_item_imgBox_itemImg" v-for="(file, index) in fileList" :key="file">
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" >
|
||||
<a-spin
|
||||
:indicator="indicator"
|
||||
tip="Uploading..."
|
||||
/>
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" style="display: flex;align-items: center;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div
|
||||
class="imgBox"
|
||||
@@ -217,9 +238,10 @@ export default defineComponent({
|
||||
exhibitionImgList:[],//选择的图片
|
||||
projectData:null,//批量id
|
||||
objectList:[],
|
||||
porjectName:'',//任务名字
|
||||
//toProduct
|
||||
generateText:'',//输入的内容
|
||||
similarity:[30,60],
|
||||
similarity:[20,40],
|
||||
brightenValue:1,//亮度
|
||||
relightDirection:'Right Light',//打光方向
|
||||
relightDirectionList:[
|
||||
@@ -238,13 +260,43 @@ export default defineComponent({
|
||||
}
|
||||
],
|
||||
fileList:[],
|
||||
uploadElement:[],
|
||||
placeholder:'',
|
||||
})
|
||||
const getUploadElement = ()=>{
|
||||
operations.loadingShow = true
|
||||
let value = {
|
||||
id:operationsData.projectData,
|
||||
moduleList:['uploadElement']
|
||||
}
|
||||
operationsData.placeholder = 'Batch_' + setPlaceholder()
|
||||
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
|
||||
operationsData.uploadElement = rv.uploadElement
|
||||
operations.loadingShow = false
|
||||
}).catch((err)=>{
|
||||
operations.loadingShow = false
|
||||
})
|
||||
}
|
||||
let init = (projectData,buildType)=>{
|
||||
operations.operationsModal = true
|
||||
if(projectData?.id)operationsData.projectData = {label:projectData.name,value:projectData.id}
|
||||
|
||||
clearData()
|
||||
if(projectData?.id){
|
||||
operationsData.projectData = {label:projectData.name,value:projectData.id}
|
||||
getUploadElement()
|
||||
}
|
||||
if(buildType.value)operationsData.buildType = buildType
|
||||
}
|
||||
const clearData = ()=>{
|
||||
operationsData.porjectName = ''
|
||||
operationsData.generateText = ''
|
||||
operationsData.similarity = [20,40]
|
||||
operationsData.brightenValue = 1
|
||||
operationsData.fileList = []
|
||||
operationsData.uploadElement = []
|
||||
}
|
||||
const changeProject = ()=>{
|
||||
getUploadElement()
|
||||
}
|
||||
const changeBuildType = ()=>{
|
||||
// operationsData.exhibitionImgList = []
|
||||
operationsData.projectData = null
|
||||
@@ -255,15 +307,20 @@ export default defineComponent({
|
||||
})
|
||||
getHistoryProjectList()
|
||||
}
|
||||
const getGenerateCloudImgList = ()=>{
|
||||
const getGenerateCloudImgList = (type)=>{
|
||||
let list = []
|
||||
if(operationsData.buildType == 'SINGLE_DESIGN'|| operationsData.buildType == 'SERIES_DESIGN')return list
|
||||
let selectList = operationsData.fileList.filter((item)=>item.isChecked)
|
||||
if(type == 'SINGLE_DESIGN'|| type == 'SERIES_DESIGN')return list
|
||||
let selectList = []
|
||||
let fileList = operationsData.fileList.filter((item)=>item.isChecked)
|
||||
let uploadElement = operationsData.uploadElement.filter((item)=>item.isChecked)
|
||||
if(fileList)selectList.push(...fileList)
|
||||
if(uploadElement)selectList.push(...uploadElement)
|
||||
|
||||
selectList.forEach((item)=>{
|
||||
let obj = {
|
||||
|
||||
}
|
||||
if(operationsData.buildType == 'POSE_TRANSFER'){
|
||||
if(type == 'POSE_TRANSFER'){
|
||||
obj = {
|
||||
poseId:1,
|
||||
productImage:getMinioUrl(item.imgUrl)
|
||||
@@ -271,7 +328,7 @@ export default defineComponent({
|
||||
}else{
|
||||
obj = {
|
||||
elementId:item.id,
|
||||
elementType:item.type
|
||||
elementType:item.type||'ProductElement'
|
||||
}
|
||||
}
|
||||
list.push(obj)
|
||||
@@ -337,10 +394,11 @@ export default defineComponent({
|
||||
"buildType": buildTypeCorresponding[operationsData.buildType],
|
||||
nums: operationsData.numberOfImages,
|
||||
projectId: operationsData.projectData,
|
||||
name:operationsData.porjectName || operationsData.projectData,
|
||||
//productimg
|
||||
toProductImage:{
|
||||
prompt:operationsData.generateText,//输入的内容
|
||||
toProductImageVOList:(operationsData.buildType == 'TO_PRODUCT_IMAGE' || operationsData.buildType == 'RELIGHT')?getGenerateCloudImgList():[],//选择的图片
|
||||
toProductImageVOList:getGenerateCloudImgList(operationsData.buildType),//选择的图片
|
||||
// toProductImageVOList:getPorductImg(),//选择的图片
|
||||
projectId: operationsData.projectData,
|
||||
direction:operationsData.relightDirection,//打光方向
|
||||
@@ -351,7 +409,7 @@ export default defineComponent({
|
||||
},
|
||||
//poseTransform
|
||||
// poseTransform:getPoseTransformData(),
|
||||
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList():[],
|
||||
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList('POSE_TRANSFER'):[],
|
||||
private: operationsData.projectData,
|
||||
ToProductImageDTO: operationsData.projectData,
|
||||
}
|
||||
@@ -375,7 +433,7 @@ export default defineComponent({
|
||||
// if(data.poseTransform.length == 0)return message.warning("You must first generate results in the 'To Product Image' module before you can use the 'Transfer Pose' cloud generation feature.")
|
||||
}
|
||||
if(operationsData.buildType == 'DESIGN' && !operationsData.projectData)return message.warning('Please select a project')
|
||||
if(!data.buildType || !data.nums || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
|
||||
if(!data.buildType || !data.nums || !data.name || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
|
||||
operations.loadingShow = true
|
||||
Https.axiosPost(Https.httpUrls.designCloud, data).then(
|
||||
(rv) => {
|
||||
@@ -458,11 +516,25 @@ export default defineComponent({
|
||||
bor = false
|
||||
}
|
||||
}
|
||||
const setPlaceholder = ()=>{
|
||||
if(!operationsData.projectData)return ''
|
||||
let index = operationsData.objectList.findIndex(item => item.id === operationsData.projectData)
|
||||
return operationsData.objectList[index].name
|
||||
}
|
||||
const focus = ()=>{
|
||||
if(operationsData.porjectName)return
|
||||
operationsData.porjectName = operationsData.placeholder
|
||||
}
|
||||
const blur = ()=>{
|
||||
if(operationsData.porjectName != operationsData.placeholder)return
|
||||
operationsData.porjectName = ''
|
||||
}
|
||||
return {
|
||||
...toRefs(operations),
|
||||
...toRefs(operationsData),
|
||||
cancelDsign,
|
||||
init,
|
||||
changeProject,
|
||||
focus,
|
||||
blur,
|
||||
setOk,
|
||||
@@ -658,13 +730,17 @@ export default defineComponent({
|
||||
// border-radius: 1.6rem;
|
||||
flex: 1;
|
||||
}
|
||||
> input{
|
||||
border-radius: 1.6rem;
|
||||
width: 4rem;
|
||||
margin-left: 1rem;
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
>div{
|
||||
input{
|
||||
border-radius: 1.6rem;
|
||||
width: 5rem;
|
||||
padding: 4px 11px 4px;
|
||||
margin-left: 1rem;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,17 +47,35 @@
|
||||
<div class="content">
|
||||
<tr v-for="(row, index) in contentList" :key="index">
|
||||
<td v-for="header in cloudTiltleList" :key="header.value">
|
||||
<span v-show="header.value != 'operation'">
|
||||
<div v-if="header.value != 'operation' && header.value != 'name'">
|
||||
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
|
||||
</div>
|
||||
<div v-if="header.value == 'name'">
|
||||
<div v-if="row.id == renameId" class="rename">
|
||||
<input type="text" v-model="renameText">
|
||||
<i class="fi fi-br-check" @click="submitRename(row)"></i>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
|
||||
</div>
|
||||
</div>
|
||||
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="setRename(row)">
|
||||
Rename
|
||||
</span>
|
||||
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="detailIamge(row)">
|
||||
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="detailIamge(row)">
|
||||
Review
|
||||
</span>
|
||||
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="deleteRom(row)">
|
||||
Delete
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
<a-pagination style="text-align: center;" @change="pagination" v-model:current="currentPage" :total="total" show-less-items />
|
||||
</div>
|
||||
<div class="mark_loading" v-show="loadingShow">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<createCloud ref="createCloud" :cloudList="generateList.seriesDesign" @getContentList="submitGetContentList"></createCloud>
|
||||
</div>
|
||||
</template>
|
||||
@@ -159,6 +177,9 @@ export default defineComponent({
|
||||
},
|
||||
cloudTiltleList:[
|
||||
{
|
||||
name:'Task Name',
|
||||
value:'name',
|
||||
},{
|
||||
name:'Task type',
|
||||
value:'buildType',
|
||||
fun:(value:any)=>{
|
||||
@@ -213,6 +234,9 @@ export default defineComponent({
|
||||
] as any,
|
||||
objectList:[],
|
||||
isGetContentList:false as any,
|
||||
renameId:-1 as any,
|
||||
renameText:'',
|
||||
loadingShow:false,
|
||||
})
|
||||
const dataDom = reactive({
|
||||
createCloud,
|
||||
@@ -259,7 +283,13 @@ export default defineComponent({
|
||||
store.commit('setCloudList',{str:'relight',list:rv.relight})
|
||||
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
|
||||
}else if(item.buildType == 'poseTransfer'){
|
||||
store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
|
||||
let list = {
|
||||
list:rv.poseTransfer,
|
||||
str:'add',
|
||||
index:-1,
|
||||
}
|
||||
store.commit("setPoseTransfer", list);
|
||||
// store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
|
||||
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
|
||||
}
|
||||
// if(rv.design && rv.design.length > 0){
|
||||
@@ -281,6 +311,7 @@ export default defineComponent({
|
||||
}
|
||||
const pagination = ()=>{
|
||||
data.isGetContentList = true
|
||||
data.renameId = -1
|
||||
getContentList()
|
||||
}
|
||||
let time = null as any
|
||||
@@ -290,7 +321,7 @@ export default defineComponent({
|
||||
let value = {
|
||||
page:data.currentPage,
|
||||
size:data.pageSize,
|
||||
projectId: data.projectData?.value,
|
||||
projectId: data.projectData?.value?data.projectData?.value:'',
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudPage,value).then((rv)=>{
|
||||
data.contentList = rv.content
|
||||
@@ -342,6 +373,40 @@ export default defineComponent({
|
||||
const handleChange = (event:any,value:any)=>{
|
||||
data.createData = value
|
||||
}
|
||||
const setRename = (item:any)=>{
|
||||
data.renameId = item.id
|
||||
data.renameText = item.name
|
||||
}
|
||||
const submitRename = (item:any)=>{
|
||||
data.renameId = -1
|
||||
data.loadingShow = true
|
||||
let value = {
|
||||
id:item.id,
|
||||
name:data.renameText,
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudTaskNameUpdate,value).then((rv)=>{
|
||||
data.loadingShow = false
|
||||
data.renameText = ''
|
||||
|
||||
data.isGetContentList = true
|
||||
getContentList()
|
||||
}).catch((err)=>{
|
||||
data.loadingShow = false
|
||||
})
|
||||
}
|
||||
const deleteRom = (item:any)=>{
|
||||
let value = {
|
||||
id:item.id
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudTaskDelete,value).then((rv)=>{
|
||||
data.loadingShow = false
|
||||
|
||||
data.isGetContentList = true
|
||||
getContentList()
|
||||
}).catch((err)=>{
|
||||
data.loadingShow = false
|
||||
})
|
||||
}
|
||||
onBeforeUnmount(()=>{
|
||||
data.isGetContentList = false
|
||||
})
|
||||
@@ -366,6 +431,9 @@ export default defineComponent({
|
||||
handleChange,
|
||||
getHistoryProjectList,
|
||||
pagination,
|
||||
setRename,
|
||||
submitRename,
|
||||
deleteRom,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
@@ -452,7 +520,31 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
width: calc(100% / 4);
|
||||
line-height: 4.6rem;
|
||||
font-size: 2.2rem;
|
||||
font-size: 1.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.rename{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input{
|
||||
height: 100%;
|
||||
padding: .8rem;
|
||||
width: 12rem;
|
||||
}
|
||||
> i{
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ import { useStore } from "vuex";
|
||||
import { Modal,message,Upload,CascaderProps } from 'ant-design-vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setCookie, getCookie, WriteCookie } from "@/tool/cookie";
|
||||
import { useRouter,useRoute } from 'vue-router'
|
||||
export default defineComponent({
|
||||
components:{
|
||||
},
|
||||
@@ -88,6 +89,7 @@ export default defineComponent({
|
||||
emits:['chatChange'],
|
||||
setup(props,{emit}) {
|
||||
const store = useStore();
|
||||
const route = useRoute()
|
||||
const data = reactive({
|
||||
chatContent:'',
|
||||
openChat:true,
|
||||
@@ -107,6 +109,7 @@ export default defineComponent({
|
||||
watch(()=>data.selectObject.id,(newValue,oldValue)=>{
|
||||
if(newValue && (data.selectObject.httpType == 'SERIES_DESIGN' || data.selectObject.httpType == 'SINGLE_DESIGN')){
|
||||
data.chatList = []
|
||||
if(route.query?.create)return
|
||||
nextTick(()=>{
|
||||
getChatHistory(newValue)
|
||||
})
|
||||
@@ -146,7 +149,7 @@ export default defineComponent({
|
||||
// data.chatList[data.chatList.length-1].content.message+=JSON.parse(event.data).content
|
||||
// }
|
||||
const container = dataDom.chatBox;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
if(container?.scrollHeight)container.scrollTop = container.scrollHeight;
|
||||
|
||||
const eventData = JSON.parse(event.data)
|
||||
if(eventData.type == 'text'){
|
||||
@@ -155,6 +158,11 @@ export default defineComponent({
|
||||
data.chatList[data.chatList.length-1].content.think+=eventData.content
|
||||
}else if(eventData.type == "tools_response"){
|
||||
let nameList = ['moodboard','printboard','sketchboard','generate_color_code']
|
||||
let nameData = {
|
||||
moodboard:'moodBoard',
|
||||
printboard:'printBoard',
|
||||
sketchboard:'sketchBoard',
|
||||
} as any
|
||||
let getData = ''
|
||||
if(nameList.indexOf(eventData.tools_name) > -1){
|
||||
if(data.chatList[data.chatList.length - 1].content.message)data.chatList.push({content:{message:''},role:'system'})
|
||||
@@ -163,16 +171,14 @@ export default defineComponent({
|
||||
getData = 'colorboard'
|
||||
}else{
|
||||
data.chatList[data.chatList.length-1].content.img = JSON.parse(JSON.parse(event.data).content).receiveCollectionElementList
|
||||
getData = eventData.tools_name
|
||||
getData = nameData[eventData.tools_name]
|
||||
}
|
||||
data.chatList.push({content:{message:''},role:'system'})
|
||||
}else{
|
||||
|
||||
}else if(eventData.tools_name == 'design_control_signal'){
|
||||
emit('chatChange',{type:eventData.tools_name,design:true})
|
||||
}
|
||||
emit('chatChange',{type:eventData.type,module:getData})
|
||||
|
||||
}else if(eventData.type == "design_control_signal"){
|
||||
emit('chatChange',{type:eventData.type,design:true})
|
||||
}
|
||||
//emit('chatChange',{type:JSON.parse(event.data).status})
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
:closable="false"
|
||||
:mask="true"
|
||||
:keyboard="false"
|
||||
:destroyOnClose="true"
|
||||
:destroyOnClose="false"
|
||||
:zIndex="1000"
|
||||
>
|
||||
<div class="generalModel_btn">
|
||||
@@ -23,18 +23,33 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collection_title">
|
||||
<div class="collection_title_text">
|
||||
<div v-show="collectionStep === 1">{{ $t('collectionModal.Moodboard') }}</div>
|
||||
<div v-show="collectionStep === 2">{{ $t('collectionModal.Printboard') }}</div>
|
||||
<div v-show="collectionStep === 3">{{ $t('collectionModal.Colorboard') }}</div>
|
||||
<div v-show="collectionStep === 4">{{ $t('collectionModal.Mannquinboard') }}</div>
|
||||
<div v-show="collectionStep === 5">{{ $t('collectionModal.Sketchboard') }}</div>
|
||||
<!-- <div v-show="collectionStep === 5">Markets Sketch</div> -->
|
||||
<!-- <div class="collection_title_text_intro" v-show="collectionStep === 1">{{ $t('collectionModal.MoodCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 2">{{ $t('collectionModal.PrinCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 3">{{ $t('collectionModal.ColorCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="designOpenrtion_content">
|
||||
<!-- <div class="modal_title_text">
|
||||
<div>Setting</div>
|
||||
</div> -->
|
||||
<div class="collectionBox">
|
||||
<MoodboardUpload ref="moodBoard" v-if="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
|
||||
<PrintboardUpload ref="printBoard" v-if="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
|
||||
<ColorboardUpload ref="colorBoard" v-if="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
|
||||
<SketchboardUpload ref="sketchBoard" v-if="openType == 'sketchBoard' || collectionStep == 4"></SketchboardUpload>
|
||||
<mannequin ref="mannequin" v-if="openType == 'mannequin' || collectionStep == 5"></mannequin>
|
||||
<MoodboardUpload ref="moodBoard" v-show="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
|
||||
<PrintboardUpload ref="printBoard" v-show="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
|
||||
<ColorboardUpload ref="colorBoard" v-show="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
|
||||
<mannequin ref="mannequin" v-show="openType == 'mannequin' || collectionStep == 4"></mannequin>
|
||||
<SketchboardUpload ref="sketchBoard" v-show="openType == 'sketchBoard' || collectionStep == 5"></SketchboardUpload>
|
||||
</div>
|
||||
<div class="collection_page">
|
||||
<div class="collection_page" v-show="isNext">
|
||||
<i v-show="collectionStep > 1" class="fi fi-rr-arrow-small-left" @click="lastStep()"></i>
|
||||
<i v-if="collectionStep < 5" class="fi fi-rr-arrow-small-right Guide_1_8" @click.stop="nextStep()"></i>
|
||||
<i v-else class="fi fi-rr-check Guide_1_14" @click.stop="cleardata()"></i>
|
||||
@@ -47,11 +62,12 @@
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive} from 'vue'
|
||||
import { defineComponent,computed,ref,provide,nextTick,inject,toRefs, reactive, onBeforeMount} from 'vue'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { Https } from "@/tool/https";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { rgbToHsv, dataURLtoBlob } from "@/tool/util";
|
||||
import { init } from 'echarts/core';
|
||||
import MoodboardUpload from './collection/MoodboardUpload.vue';
|
||||
import PrintboardUpload from './collection/PrintboardUpload.vue';
|
||||
@@ -64,6 +80,10 @@ export default defineComponent({
|
||||
MoodboardUpload,PrintboardUpload,ColorboardUpload,SketchboardUpload,mannequin,
|
||||
},
|
||||
props:{
|
||||
isNext:{
|
||||
type:Boolean,
|
||||
default:true,
|
||||
},
|
||||
},
|
||||
emits:['getHistory'],
|
||||
setup(props,{emit}) {
|
||||
@@ -74,6 +94,8 @@ export default defineComponent({
|
||||
openType:'',
|
||||
collectionStep:1,
|
||||
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
|
||||
createProbject:inject('createProbject') as any
|
||||
|
||||
})
|
||||
let driver__:any = computed(()=>{
|
||||
return store.state.Guide.guide
|
||||
@@ -83,8 +105,8 @@ export default defineComponent({
|
||||
moodBoard:null as any,
|
||||
printBoard:null as any,
|
||||
colorBoard:null as any,
|
||||
sketchBoard:null as any,
|
||||
mannequin:null as any,
|
||||
sketchBoard:null as any,
|
||||
}) as any
|
||||
const init = (value:any)=>{
|
||||
data.habitSetStyle = true
|
||||
@@ -95,14 +117,16 @@ export default defineComponent({
|
||||
dataDom[value].openSetData()
|
||||
})
|
||||
}
|
||||
let cleardata = ()=>{
|
||||
let cleardata = async ()=>{
|
||||
data.habitSetStyle = false
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = 1
|
||||
if(data.openType)store.dispatch('setAllBoardData',{type:data.openType})
|
||||
saveProject(data.openType)
|
||||
}
|
||||
const saveProject = (str:any)=>{
|
||||
const saveProject = async (str:any)=>{
|
||||
if(str == 'design')return
|
||||
if(!data.selectObject.id && data.createProbject)await data.createProbject()
|
||||
let value:any = {
|
||||
projectId:data.selectObject.id,
|
||||
}
|
||||
@@ -114,11 +138,49 @@ export default defineComponent({
|
||||
})
|
||||
})
|
||||
}
|
||||
let lastStep = ()=>{
|
||||
let getPantongName = ()=>{
|
||||
let colorBoards = store.state.UploadFilesModule.colorBoards;
|
||||
if(!colorBoards || colorBoards?.length == 0) return
|
||||
data.isShowMark = true
|
||||
let value: any = [];
|
||||
for (let v of colorBoards) {
|
||||
let color: any = [v.rgbValue.r, v.rgbValue.g, v.rgbValue.b];
|
||||
let hsv = rgbToHsv(color);
|
||||
v.hsv = hsv[0] + hsv[1] + hsv[2];
|
||||
value.push({
|
||||
h: hsv[0],
|
||||
s: hsv[1],
|
||||
v: hsv[2],
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve: any, reject: any) => {
|
||||
Https.axiosPost(Https.httpUrls.getRgbByHsvBatch, value)
|
||||
.then((rv: any) => {
|
||||
if (rv) {
|
||||
rv.forEach((ele: any, index: number) => {
|
||||
colorBoards[index].id = ele.id;
|
||||
colorBoards[index].tcx = ele.tcx;
|
||||
colorBoards[index].name = ele.name;
|
||||
});
|
||||
store.commit("setColorboardList", colorBoards);
|
||||
resolve();
|
||||
}
|
||||
data.isShowMark = false
|
||||
})
|
||||
.catch((res) => {
|
||||
reject();
|
||||
data.isShowMark = false
|
||||
});
|
||||
});
|
||||
}
|
||||
let lastStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep - 1
|
||||
setOpenSetData()
|
||||
}
|
||||
let nextStep = ()=>{
|
||||
let nextStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep + 1
|
||||
setOpenSetData()
|
||||
}
|
||||
@@ -150,6 +212,42 @@ export default defineComponent({
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.collectionModal{
|
||||
:deep(.ant-modal-body){
|
||||
padding-top: 10rem;
|
||||
|
||||
}
|
||||
.collection_title{
|
||||
top: calc(4rem*1.2);
|
||||
display: flex;
|
||||
font-size: var(--aida-fsize2);
|
||||
font-weight: 900;
|
||||
color: rgba(0,0,0,.65);
|
||||
z-index: 999;
|
||||
align-items: center;
|
||||
width: calc(35rem*1.2);
|
||||
justify-content: space-between;
|
||||
.collection_progress{
|
||||
width: calc(8rem*1.2);
|
||||
height: calc(8rem*1.2);
|
||||
>div{
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
:deep(.ant-progress-inner){
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.collection_title_text{
|
||||
// margin-right: calc(4rem*1.2);
|
||||
}
|
||||
.collection_title_text_intro{
|
||||
font-size: var(--aida-fsize1-4);
|
||||
font-weight: 400;
|
||||
color: rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
}
|
||||
:deep(.designOpenrtion_content){
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -560,7 +560,7 @@ export default defineComponent({
|
||||
level2Type:'',
|
||||
designType:'',
|
||||
}
|
||||
let arr = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) || []
|
||||
let arr = this.store.state.UploadFilesModule.allBoardData.printboardFiles?JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) : []
|
||||
let setboard = {
|
||||
generate:[] as any,
|
||||
material:[] as any,
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>Selection Function</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
|
||||
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
</div>
|
||||
@@ -44,23 +50,16 @@
|
||||
</a-slider>
|
||||
<input type="number" readonly v-model="productimgSimilarity">
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
@afterChange="setSimilarity"
|
||||
:tooltipVisible="false"
|
||||
:step="5"
|
||||
>
|
||||
</a-slider> -->
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
|
||||
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Highlight')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
|
||||
<a-slider class="system_silder"
|
||||
v-model:value="productimgBrightenValue"
|
||||
:tooltipVisible="false"
|
||||
@@ -109,7 +108,7 @@
|
||||
<div class="generage_btn_box" style="margin-left: auto;">
|
||||
<div class="generage_btn started_btn" v-show="!generateSuccess.productimgIsProductimg">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType == 'PoseTransfer'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
@@ -237,19 +236,37 @@ setup(props:any,{emit}) {
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
] as any,
|
||||
speedTypeList:{
|
||||
poseTransfer:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
toPorductImg:[
|
||||
{
|
||||
title:'Generate with high quality',
|
||||
label:'High',
|
||||
relightLabel:'Relight',
|
||||
value:'',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
relightLabel:'Edit',
|
||||
value:'flux',
|
||||
},
|
||||
]
|
||||
},
|
||||
speedState:false,
|
||||
speedData:{
|
||||
title:'Generate high-quality images',
|
||||
relightLabel:'Relight',
|
||||
label:'High',
|
||||
value:'',
|
||||
},
|
||||
@@ -264,6 +281,7 @@ setup(props:any,{emit}) {
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
let scaleImage: any = ref(false);
|
||||
let isShowMark = ref(false)
|
||||
@@ -316,15 +334,17 @@ setup(props:any,{emit}) {
|
||||
direction:productimg.productimgRelightDirection,
|
||||
prompt:productimg.productimgSearchName,
|
||||
toProductImageVOList:[obj],
|
||||
modelName:speed.speedData.value,
|
||||
brightenValue:productimg.productimgBrightenValue,
|
||||
projectId:productimg.selectObject.id,
|
||||
imageStrength:(100 - imageStrength)/100,
|
||||
ageGroup:productimg.selectObject.ageGroup
|
||||
}
|
||||
// return
|
||||
productimg.generateSuccess.productimgIsProductimg = true
|
||||
productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
|
||||
productimg.generateSuccess.productimgRemProductimg = true
|
||||
},10000)
|
||||
// productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
|
||||
// productimg.generateSuccess.productimgRemProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.relight
|
||||
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
|
||||
url = Https.httpUrls.toProduct
|
||||
@@ -337,6 +357,7 @@ setup(props:any,{emit}) {
|
||||
productimg.generateSuccess.isShowMark = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productimg.generateSuccess.productimgRemProductimg = true
|
||||
productimg.generateSuccess.isShowMark = false
|
||||
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
|
||||
let arr:any = []
|
||||
@@ -484,7 +505,8 @@ setup(props:any,{emit}) {
|
||||
oldId:productimg.generateSuccess.id,
|
||||
status:productimg.generateSuccess.status,
|
||||
listType:productimg.generateSuccess.listType,
|
||||
isIndex:productimg.generateSuccess.isIndex
|
||||
isIndex:productimg.generateSuccess.isIndex,
|
||||
userLikeSortId:productimg.generateSuccess.userLikeSortId
|
||||
}
|
||||
emit('addGenerateImg',data)
|
||||
|
||||
@@ -565,7 +587,8 @@ methods: {
|
||||
this.generateSuccess.isIndex = index
|
||||
|
||||
}
|
||||
|
||||
// this.
|
||||
this.generateSuccess.userLikeSortId = list[index].userLikeSortId
|
||||
this.generateSuccess.productimgIsProductimg = !!this.generateSuccess.productimgIsProductimg
|
||||
this.generateSuccess.productimgRemProductimg = !!this.generateSuccess.productimgRemProductimg
|
||||
this.generateSuccess.isShowMark = !!this.generateSuccess.isShowMark
|
||||
@@ -595,6 +618,12 @@ methods: {
|
||||
this.scaleImage = true
|
||||
this.isGenerate = false
|
||||
this.scaleImageList = list
|
||||
if(this.scaleImageList[index].resultType == "PoseTransfer"){
|
||||
this.speedList = this.speedTypeList.poseTransfer
|
||||
}else{
|
||||
this.speedList = this.speedTypeList.toPorductImg
|
||||
}
|
||||
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
|
||||
// if(this.scaleImageList[index]?.resultType == 'ToProductImage')this.scaleImageList[index].sourceUrl = this.scaleImageList[index].imgUrl
|
||||
this.scaleImageIndex = index
|
||||
if(dialogueIndex)this.robotAssits = dialogueIndex
|
||||
|
||||