diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..b2abc8d --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,67 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useSlots": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true + } +} diff --git a/auto-imports.d.ts b/auto-imports.d.ts new file mode 100644 index 0000000..d5e7dcf --- /dev/null +++ b/auto-imports.d.ts @@ -0,0 +1,78 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: typeof import("vue")["EffectScope"]; + const computed: typeof import("vue")["computed"]; + const createApp: typeof import("vue")["createApp"]; + const customRef: typeof import("vue")["customRef"]; + const defineAsyncComponent: typeof import("vue")["defineAsyncComponent"]; + const defineComponent: typeof import("vue")["defineComponent"]; + const effectScope: typeof import("vue")["effectScope"]; + const getCurrentInstance: typeof import("vue")["getCurrentInstance"]; + const getCurrentScope: typeof import("vue")["getCurrentScope"]; + const h: typeof import("vue")["h"]; + const inject: typeof import("vue")["inject"]; + const isProxy: typeof import("vue")["isProxy"]; + const isReactive: typeof import("vue")["isReactive"]; + const isReadonly: typeof import("vue")["isReadonly"]; + const isRef: typeof import("vue")["isRef"]; + const markRaw: typeof import("vue")["markRaw"]; + const nextTick: typeof import("vue")["nextTick"]; + const onActivated: typeof import("vue")["onActivated"]; + const onBeforeMount: typeof import("vue")["onBeforeMount"]; + const onBeforeUnmount: typeof import("vue")["onBeforeUnmount"]; + const onBeforeUpdate: typeof import("vue")["onBeforeUpdate"]; + const onDeactivated: typeof import("vue")["onDeactivated"]; + const onErrorCaptured: typeof import("vue")["onErrorCaptured"]; + const onMounted: typeof import("vue")["onMounted"]; + const onRenderTracked: typeof import("vue")["onRenderTracked"]; + const onRenderTriggered: typeof import("vue")["onRenderTriggered"]; + const onScopeDispose: typeof import("vue")["onScopeDispose"]; + const onServerPrefetch: typeof import("vue")["onServerPrefetch"]; + const onUnmounted: typeof import("vue")["onUnmounted"]; + const onUpdated: typeof import("vue")["onUpdated"]; + const provide: typeof import("vue")["provide"]; + const reactive: typeof import("vue")["reactive"]; + const readonly: typeof import("vue")["readonly"]; + const ref: typeof import("vue")["ref"]; + const resolveComponent: typeof import("vue")["resolveComponent"]; + const shallowReactive: typeof import("vue")["shallowReactive"]; + const shallowReadonly: typeof import("vue")["shallowReadonly"]; + const shallowRef: typeof import("vue")["shallowRef"]; + const toRaw: typeof import("vue")["toRaw"]; + const toRef: typeof import("vue")["toRef"]; + const toRefs: typeof import("vue")["toRefs"]; + const toValue: typeof import("vue")["toValue"]; + const triggerRef: typeof import("vue")["triggerRef"]; + const unref: typeof import("vue")["unref"]; + const useAttrs: typeof import("vue")["useAttrs"]; + const useCssModule: typeof import("vue")["useCssModule"]; + const useCssVars: typeof import("vue")["useCssVars"]; + const useSlots: typeof import("vue")["useSlots"]; + const watch: typeof import("vue")["watch"]; + const watchEffect: typeof import("vue")["watchEffect"]; + const watchPostEffect: typeof import("vue")["watchPostEffect"]; + const watchSyncEffect: typeof import("vue")["watchSyncEffect"]; +} +// for type re-export +declare global { + // @ts-ignore + export type { + Component, + ComponentPublicInstance, + ComputedRef, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + VNode, + WritableComputedRef + } from "vue"; + import("vue"); +} diff --git a/locales/en.yaml b/locales/en.yaml index 80b4e67..987a08f 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -46,3 +46,6 @@ login: usernameReg: Please enter username passwordReg: Please enter password passwordRuleReg: The password format should be any combination of 8-18 digits +flip: + x: flip x + y: flip y diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index a6f434b..c960911 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -52,3 +52,6 @@ login: usernameReg: 请输入账号 passwordReg: 请输入密码 passwordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合 +flip: + x: 水平翻转 + y: 垂直翻转 diff --git a/package.json b/package.json index 7816053..d9f216b 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,12 @@ "echarts": "^5.4.2", "echarts-gl": "^2.0.9", "element-plus": "2.3.6", + "events": "^3.3.0", "fabric": "^5.3.0", + "hotkeys-js": "^3.13.7", "js-cookie": "^3.0.5", "lib-flexible": "^0.3.2", + "lodash-es": "^4.17.21", "lottie-web": "^5.12.2", "mitt": "^3.0.0", "mockjs": "^1.1.0", @@ -54,12 +57,15 @@ "responsive-storage": "^2.2.0", "sortablejs": "^1.15.0", "swiper": "^11.0.5", + "tapable": "^2.2.1", + "uuid": "^10.0.0", "v3-infinite-loading": "^1.2.2", "vue": "^3.3.4", "vue-i18n": "^9.2.2", "vue-router": "^4.2.2", "vue-types": "^5.1.0", "vue-waterfall-plugin-next": "^2.2.1", + "vue3-lazyload": "^0.3.8", "vue3-scale-box": "^0.1.9" }, "devDependencies": { @@ -105,6 +111,8 @@ "tailwindcss": "^3.3.2", "terser": "^5.18.1", "typescript": "5.0.4", + "unplugin-auto-import": "^0.18.2", + "unplugin-vue-components": "^0.27.3", "vite": "^4.3.9", "vite-plugin-cdn-import": "^0.3.5", "vite-plugin-compression": "^0.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b47938..d5383f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,15 +41,24 @@ dependencies: element-plus: specifier: 2.3.6 version: 2.3.6(vue@3.3.4) + events: + specifier: ^3.3.0 + version: 3.3.0 fabric: specifier: ^5.3.0 version: 5.3.0 + hotkeys-js: + specifier: ^3.13.7 + version: 3.13.7 js-cookie: specifier: ^3.0.5 version: 3.0.5 lib-flexible: specifier: ^0.3.2 version: 0.3.2 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 lottie-web: specifier: ^5.12.2 version: 5.12.2 @@ -83,6 +92,12 @@ dependencies: swiper: specifier: ^11.0.5 version: 11.0.5 + tapable: + specifier: ^2.2.1 + version: 2.2.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 v3-infinite-loading: specifier: ^1.2.2 version: 1.2.2 @@ -101,12 +116,12 @@ dependencies: vue-waterfall-plugin-next: specifier: ^2.2.1 version: 2.2.1(vue@3.3.4) + vue3-lazyload: + specifier: ^0.3.8 + version: 0.3.8(vue@3.3.4) vue3-scale-box: specifier: ^0.1.9 version: 0.1.9 - 安装插件: - specifier: link://安装插件 - version: link:../../../../安装插件 devDependencies: "@commitlint/cli": @@ -235,6 +250,12 @@ devDependencies: typescript: specifier: 5.0.4 version: 5.0.4 + unplugin-auto-import: + specifier: ^0.18.2 + version: 0.18.2(@vueuse/core@10.2.0) + unplugin-vue-components: + specifier: ^0.27.3 + version: 0.27.3(vue@3.3.4) vite: specifier: ^4.3.9 version: 4.3.9(@types/node@20.3.1)(sass@1.63.6)(terser@5.18.1) @@ -279,6 +300,13 @@ packages: "@jridgewell/gen-mapping": 0.3.3 "@jridgewell/trace-mapping": 0.3.18 + /@antfu/utils@0.7.10: + resolution: + { + integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww== + } + dev: true + /@babel/code-frame@7.22.5: resolution: { @@ -1559,6 +1587,13 @@ packages: integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== } + /@jridgewell/sourcemap-codec@1.5.0: + resolution: + { + integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + } + dev: true + /@jridgewell/trace-mapping@0.3.18: resolution: { @@ -1824,6 +1859,23 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 + /@rollup/pluginutils@5.1.0: + resolution: + { + integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== + } + engines: { node: ">=14.0.0" } + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + "@types/estree": 1.0.1 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + /@sxzz/popperjs-es@2.11.7: resolution: { @@ -2003,7 +2055,6 @@ packages: { integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA== } - dev: false /@types/web-bluetooth@0.0.20: resolution: @@ -2450,7 +2501,6 @@ packages: transitivePeerDependencies: - "@vue/composition-api" - vue - dev: false /@vueuse/core@10.8.0(vue@3.3.4): resolution: @@ -2476,7 +2526,7 @@ packages: "@types/web-bluetooth": 0.0.16 "@vueuse/metadata": 9.13.0 "@vueuse/shared": 9.13.0(vue@3.3.4) - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.7(vue@3.3.4) transitivePeerDependencies: - "@vue/composition-api" - vue @@ -2487,7 +2537,6 @@ packages: { integrity: sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw== } - dev: false /@vueuse/metadata@10.8.0: resolution: @@ -2532,11 +2581,10 @@ packages: integrity: sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg== } dependencies: - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.7(vue@3.3.4) transitivePeerDependencies: - "@vue/composition-api" - vue - dev: false /@vueuse/shared@10.8.0(vue@3.3.4): resolution: @@ -2556,7 +2604,7 @@ packages: integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw== } dependencies: - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.7(vue@3.3.4) transitivePeerDependencies: - "@vue/composition-api" - vue @@ -2666,6 +2714,14 @@ packages: engines: { node: ">=0.4.0" } hasBin: true + /acorn@8.12.1: + resolution: + { + integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + } + engines: { node: ">=0.4.0" } + hasBin: true + /acorn@8.9.0: resolution: { @@ -3194,6 +3250,24 @@ packages: optionalDependencies: fsevents: 2.3.2 + /chokidar@3.6.0: + resolution: + { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + } + engines: { node: ">= 8.10.0" } + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /chownr@2.0.0: resolution: { @@ -3409,6 +3483,12 @@ packages: integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== } + /confbox@0.1.7: + resolution: + { + integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + } + /connect@3.7.0: resolution: { @@ -3900,6 +3980,21 @@ packages: dependencies: ms: 2.1.2 + /debug@4.3.6: + resolution: + { + integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /decamelize-keys@1.1.1: resolution: { @@ -4375,8 +4470,6 @@ packages: } engines: { node: ">=12" } requiresBuild: true - dev: false - optional: true /escodegen@2.0.0: resolution: @@ -4633,8 +4726,6 @@ packages: requiresBuild: true dependencies: "@types/estree": 1.0.1 - dev: false - optional: true /esutils@2.0.3: resolution: @@ -4643,6 +4734,14 @@ packages: } engines: { node: ">=0.10.0" } + /events@3.3.0: + resolution: + { + integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + } + engines: { node: ">=0.8.x" } + dev: false + /execa@4.1.0: resolution: { @@ -4740,6 +4839,20 @@ packages: merge2: 1.4.1 micromatch: 4.0.5 + /fast-glob@3.3.2: + resolution: + { + integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + } + engines: { node: ">=8.6.0" } + dependencies: + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-json-stable-stringify@2.1.0: resolution: { @@ -5302,6 +5415,13 @@ packages: lru-cache: 6.0.0 dev: true + /hotkeys-js@3.13.7: + resolution: + { + integrity: sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ== + } + dev: false + /html-encoding-sniffer@3.0.0: resolution: { @@ -5714,6 +5834,13 @@ packages: } dev: true + /js-tokens@9.0.0: + resolution: + { + integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== + } + dev: true + /js-yaml@4.1.0: resolution: { @@ -5974,6 +6101,17 @@ packages: dev: false optional: true + /local-pkg@0.5.0: + resolution: + { + integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + } + engines: { node: ">=14" } + dependencies: + mlly: 1.7.1 + pkg-types: 1.0.3 + dev: true + /locate-path@5.0.0: resolution: { @@ -6162,6 +6300,15 @@ packages: dependencies: "@jridgewell/sourcemap-codec": 1.4.15 + /magic-string@0.30.11: + resolution: + { + integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== + } + dependencies: + "@jridgewell/sourcemap-codec": 1.5.0 + dev: true + /make-dir@3.1.0: resolution: { @@ -6350,6 +6497,16 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.5: + resolution: + { + integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + } + engines: { node: ">=16 || 14 >=14.17" } + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: { @@ -6441,6 +6598,17 @@ packages: pkg-types: 1.0.3 ufo: 1.1.2 + /mlly@1.7.1: + resolution: + { + integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== + } + dependencies: + acorn: 8.12.1 + pathe: 1.1.2 + pkg-types: 1.1.3 + ufo: 1.5.4 + /mockjs@1.1.0: resolution: { @@ -6979,6 +7147,12 @@ packages: integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== } + /pathe@1.1.2: + resolution: + { + integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + } + /perfect-debounce@1.0.0: resolution: { @@ -7062,9 +7236,19 @@ packages: requiresBuild: true dependencies: jsonc-parser: 3.2.0 - mlly: 1.4.0 + mlly: 1.7.1 pathe: 1.1.1 + /pkg-types@1.1.3: + resolution: + { + integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA== + } + dependencies: + confbox: 0.1.7 + mlly: 1.7.1 + pathe: 1.1.2 + /popmotion@11.0.5: resolution: { @@ -8457,6 +8641,13 @@ packages: dev: false optional: true + /scule@1.3.0: + resolution: + { + integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g== + } + dev: true + /semver@5.7.1: resolution: { @@ -8862,6 +9053,15 @@ packages: dev: false optional: true + /strip-literal@2.1.0: + resolution: + { + integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== + } + dependencies: + js-tokens: 9.0.0 + dev: true + /style-value-types@5.1.2: resolution: { @@ -9045,6 +9245,14 @@ packages: dev: false optional: true + /tapable@2.2.1: + resolution: + { + integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + } + engines: { node: ">=6" } + dev: false + /tar@6.1.15: resolution: { @@ -9339,6 +9547,12 @@ packages: } requiresBuild: true + /ufo@1.5.4: + resolution: + { + integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + } + /unctx@2.3.1: resolution: { @@ -9376,6 +9590,29 @@ packages: dev: false optional: true + /unimport@3.10.0: + resolution: + { + integrity: sha512-/UvKRfWx3mNDWwWQhR62HsoM3wxHwYdTq8ellZzMOHnnw4Dp8tovgthyW7DjTrbjDL+i4idOp06voz2VKlvrLw== + } + dependencies: + "@rollup/pluginutils": 5.1.0 + acorn: 8.12.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + mlly: 1.7.1 + pathe: 1.1.2 + pkg-types: 1.1.3 + scule: 1.3.0 + strip-literal: 2.1.0 + unplugin: 1.12.0 + transitivePeerDependencies: + - rollup + dev: true + /universalify@0.2.0: resolution: { @@ -9401,6 +9638,79 @@ packages: engines: { node: ">= 0.8" } dev: true + /unplugin-auto-import@0.18.2(@vueuse/core@10.2.0): + resolution: + { + integrity: sha512-Dwb3rAic75harVBrVjwiq6H24PT+nBq2dpxV5BH8NNI6sDFaTytvP+iyo4xy7prQbR3r5K6nMs4f5Wp9PE4g8A== + } + engines: { node: ">=14" } + peerDependencies: + "@nuxt/kit": ^3.2.2 + "@vueuse/core": "*" + peerDependenciesMeta: + "@nuxt/kit": + optional: true + "@vueuse/core": + optional: true + dependencies: + "@antfu/utils": 0.7.10 + "@rollup/pluginutils": 5.1.0 + "@vueuse/core": 10.2.0(vue@3.3.4) + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + unimport: 3.10.0 + unplugin: 1.12.0 + transitivePeerDependencies: + - rollup + dev: true + + /unplugin-vue-components@0.27.3(vue@3.3.4): + resolution: + { + integrity: sha512-5wg7lbdg5ZcrAQNzyYK+6gcg/DG8K6rO+f5YeuvqGHs/PhpapBvpA4O/0ex/pFthE5WgRk43iWuRZEMLVsdz4Q== + } + engines: { node: ">=14" } + peerDependencies: + "@babel/parser": ^7.15.8 + "@nuxt/kit": ^3.2.2 + vue: 2 || 3 + peerDependenciesMeta: + "@babel/parser": + optional: true + "@nuxt/kit": + optional: true + dependencies: + "@antfu/utils": 0.7.10 + "@rollup/pluginutils": 5.1.0 + chokidar: 3.6.0 + debug: 4.3.6 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + mlly: 1.7.1 + unplugin: 1.12.0 + vue: 3.3.4 + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /unplugin@1.12.0: + resolution: + { + integrity: sha512-KeczzHl2sATPQUx1gzo+EnUkmN4VmGBYRRVOZSGvGITE9rGHRDGqft6ONceP3vgXcyJ2XjX5axG5jMWUwNCYLw== + } + engines: { node: ">=14.0.0" } + dependencies: + acorn: 8.12.1 + chokidar: 3.6.0 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.6.2 + dev: true + /unplugin@1.3.1: resolution: { @@ -9488,6 +9798,14 @@ packages: engines: { node: ">= 0.4.0" } dev: true + /uuid@10.0.0: + resolution: + { + integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + } + hasBin: true + dev: false + /uuid@8.3.2: resolution: { @@ -9628,6 +9946,24 @@ packages: fsevents: 2.3.2 dev: true + /vue-demi@0.12.5(vue@3.3.4): + resolution: + { + integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q== + } + engines: { node: ">=12" } + hasBin: true + requiresBuild: true + peerDependencies: + "@vue/composition-api": ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + "@vue/composition-api": + optional: true + dependencies: + vue: 3.3.4 + dev: false + /vue-demi@0.14.5(vue@3.3.4): resolution: { @@ -9644,7 +9980,6 @@ packages: optional: true dependencies: vue: 3.3.4 - dev: false /vue-demi@0.14.7(vue@3.3.4): resolution: @@ -9662,7 +9997,6 @@ packages: optional: true dependencies: vue: 3.3.4 - dev: false /vue-eslint-parser@9.3.1(eslint@8.43.0): resolution: @@ -9780,6 +10114,22 @@ packages: - vue dev: false + /vue3-lazyload@0.3.8(vue@3.3.4): + resolution: + { + integrity: sha512-UiJHRT7mzry102WbhtrRgJh+f8Z8u4Z+H1RU4dvPmQeq7wFSDFxZB9iJOWGihH2FscXN/8rMGLDOQJAmjwqpCg== + } + peerDependencies: + "@vue/composition-api": ^1.0.0-rc.1 + vue: ^2.0.0 || >=3.0.0 + peerDependenciesMeta: + "@vue/composition-api": + optional: true + dependencies: + vue: 3.3.4 + vue-demi: 0.12.5(vue@3.3.4) + dev: false + /vue3-scale-box@0.1.9: resolution: { @@ -9851,6 +10201,13 @@ packages: integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== } + /webpack-virtual-modules@0.6.2: + resolution: + { + integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + } + dev: true + /whatwg-encoding@2.0.0: resolution: { diff --git a/src/assets/editor/edgecontrol.svg b/src/assets/editor/edgecontrol.svg new file mode 100644 index 0000000..8fe63b6 --- /dev/null +++ b/src/assets/editor/edgecontrol.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/editor/middlecontrol.svg b/src/assets/editor/middlecontrol.svg new file mode 100644 index 0000000..53fb5b5 --- /dev/null +++ b/src/assets/editor/middlecontrol.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/editor/middlecontrolhoz.svg b/src/assets/editor/middlecontrolhoz.svg new file mode 100644 index 0000000..ba05d9a --- /dev/null +++ b/src/assets/editor/middlecontrolhoz.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/editor/rotateicon.svg b/src/assets/editor/rotateicon.svg new file mode 100644 index 0000000..7b1d4db --- /dev/null +++ b/src/assets/editor/rotateicon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/filters/BlackWhite.png b/src/assets/filters/BlackWhite.png new file mode 100644 index 0000000..f6dc3ce Binary files /dev/null and b/src/assets/filters/BlackWhite.png differ diff --git a/src/assets/filters/Brownie.png b/src/assets/filters/Brownie.png new file mode 100644 index 0000000..30c9ab2 Binary files /dev/null and b/src/assets/filters/Brownie.png differ diff --git a/src/assets/filters/Invert.png b/src/assets/filters/Invert.png new file mode 100644 index 0000000..85b3367 Binary files /dev/null and b/src/assets/filters/Invert.png differ diff --git a/src/assets/filters/Kodachrome.png b/src/assets/filters/Kodachrome.png new file mode 100644 index 0000000..0f82e3e Binary files /dev/null and b/src/assets/filters/Kodachrome.png differ diff --git a/src/assets/filters/Polaroid.png b/src/assets/filters/Polaroid.png new file mode 100644 index 0000000..605f870 Binary files /dev/null and b/src/assets/filters/Polaroid.png differ diff --git a/src/assets/filters/Sepia.png b/src/assets/filters/Sepia.png new file mode 100644 index 0000000..a1e1071 Binary files /dev/null and b/src/assets/filters/Sepia.png differ diff --git a/src/assets/filters/Vintage.png b/src/assets/filters/Vintage.png new file mode 100644 index 0000000..1c2a877 Binary files /dev/null and b/src/assets/filters/Vintage.png differ diff --git a/src/assets/filters/technicolor.png b/src/assets/filters/technicolor.png new file mode 100644 index 0000000..a31d4a9 Binary files /dev/null and b/src/assets/filters/technicolor.png differ diff --git a/src/assets/fonts/cn/华康金刚黑.ttf b/src/assets/fonts/cn/华康金刚黑.ttf new file mode 100644 index 0000000..c0f598b Binary files /dev/null and b/src/assets/fonts/cn/华康金刚黑.ttf differ diff --git a/src/assets/fonts/cn/汉体.ttf b/src/assets/fonts/cn/汉体.ttf new file mode 100644 index 0000000..3729151 Binary files /dev/null and b/src/assets/fonts/cn/汉体.ttf differ diff --git a/src/assets/fonts/font.css b/src/assets/fonts/font.css new file mode 100644 index 0000000..ed7882e --- /dev/null +++ b/src/assets/fonts/font.css @@ -0,0 +1,9 @@ +@font-face { + font-family: "汉体"; + src: url("./cn/汉体.ttf"); +} + +@font-face { + font-family: "华康金刚黑"; + src: url("./cn/华康金刚黑.ttf"); +} diff --git a/src/assets/fonts/font.js b/src/assets/fonts/font.js new file mode 100644 index 0000000..a101282 --- /dev/null +++ b/src/assets/fonts/font.js @@ -0,0 +1,22 @@ +/* + * @Author: 秦少卫 + * @Date: 2022-09-05 22:54:14 + * @LastEditors: 秦少卫 + * @LastEditTime: 2022-09-05 22:59:30 + * @Description: 字体文件列表 + */ + +const cnList = [ + { + name: "汉体", + fontFamily: "汉体" + }, + { + name: "华康金刚黑", + fontFamily: "华康金刚黑" + } +]; + +const enList = []; + +export default [...cnList, ...enList]; diff --git a/src/assets/modelSetting/camera.svg b/src/assets/modelSetting/camera.svg new file mode 100644 index 0000000..f3158f8 --- /dev/null +++ b/src/assets/modelSetting/camera.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/modelSetting/camera1.svg b/src/assets/modelSetting/camera1.svg new file mode 100644 index 0000000..da532be --- /dev/null +++ b/src/assets/modelSetting/camera1.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/modelSetting/greenCamera.svg b/src/assets/modelSetting/greenCamera.svg new file mode 100644 index 0000000..940bb83 --- /dev/null +++ b/src/assets/modelSetting/greenCamera.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/assets/modelSetting/watchError.svg b/src/assets/modelSetting/watchError.svg new file mode 100644 index 0000000..3b49906 --- /dev/null +++ b/src/assets/modelSetting/watchError.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchErrorSelected.svg b/src/assets/modelSetting/watchErrorSelected.svg new file mode 100644 index 0000000..7b99bf8 --- /dev/null +++ b/src/assets/modelSetting/watchErrorSelected.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchOnline.svg b/src/assets/modelSetting/watchOnline.svg new file mode 100644 index 0000000..879357c --- /dev/null +++ b/src/assets/modelSetting/watchOnline.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/modelSetting/watchOnlineSelected.svg b/src/assets/modelSetting/watchOnlineSelected.svg new file mode 100644 index 0000000..9767aed --- /dev/null +++ b/src/assets/modelSetting/watchOnlineSelected.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchOutline.svg b/src/assets/modelSetting/watchOutline.svg new file mode 100644 index 0000000..a0ff988 --- /dev/null +++ b/src/assets/modelSetting/watchOutline.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/modelSetting/watchOutlineSelected.svg b/src/assets/modelSetting/watchOutlineSelected.svg new file mode 100644 index 0000000..fe37846 --- /dev/null +++ b/src/assets/modelSetting/watchOutlineSelected.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchWarn.svg b/src/assets/modelSetting/watchWarn.svg new file mode 100644 index 0000000..e4f79c1 --- /dev/null +++ b/src/assets/modelSetting/watchWarn.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchWarnSelected.svg b/src/assets/modelSetting/watchWarnSelected.svg new file mode 100644 index 0000000..7fd4575 --- /dev/null +++ b/src/assets/modelSetting/watchWarnSelected.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/modelSetting/watchtest.svg b/src/assets/modelSetting/watchtest.svg new file mode 100644 index 0000000..27d8a56 --- /dev/null +++ b/src/assets/modelSetting/watchtest.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/config/attribute/animationType.ts b/src/config/attribute/animationType.ts new file mode 100644 index 0000000..9720be7 --- /dev/null +++ b/src/config/attribute/animationType.ts @@ -0,0 +1,27 @@ +// 动效类型 +export const animationTypeConf: Record[] = [ + { + value: "None", + label: "None" + }, + { + value: "Fade", + label: "Fade" + }, + { + value: "Bounce", + label: "Bounce" + }, + { + value: "Shake", + label: "Shake" + }, + { + value: "Rotation", + label: "Rotation" + }, + { + value: "Flash", + label: "Flash" + } +]; diff --git a/src/config/attribute/baseType.ts b/src/config/attribute/baseType.ts new file mode 100644 index 0000000..e4b8633 --- /dev/null +++ b/src/config/attribute/baseType.ts @@ -0,0 +1,103 @@ +// 通用元素 +export const baseTypeConf: string[] = [ + "text", + "i-text", + "textbox", + "rect", + "circle", + "triangle", + "polygon", + "image", + "group", + "line", + "arrow" +]; + +// 文字元素 +export const textTypeConf: string[] = ["i-text", "textbox", "text"]; + +// 字体下拉列表 +export const strokeDashListConf: Record[] = [ + { + value: { + strokeUniform: true, + strokeDashArray: [], + strokeLineCap: "butt" + }, + label: "Stroke" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [1, 10], + strokeLineCap: "butt" + }, + label: "Dash-1" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [1, 10], + strokeLineCap: "round" + }, + label: "Dash-2" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [15, 15], + strokeLineCap: "square" + }, + label: "Dash-3" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [15, 15], + strokeLineCap: "round" + }, + label: "Dash-4" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [25, 25], + strokeLineCap: "square" + }, + label: "Dash-5" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [25, 25], + strokeLineCap: "round" + }, + label: "Dash-6" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [1, 8, 16, 8, 1, 20], + strokeLineCap: "square" + }, + label: "Dash-7" + }, + { + value: { + strokeUniform: true, + strokeDashArray: [1, 8, 16, 8, 1, 20], + strokeLineCap: "round" + }, + label: "Dash-8" + } +]; + +// 对齐图标 +export const textAlignListSvgConf: string[] = [ + '', + '', + '' +]; + +// 字体对齐方式 +export const textAlignListConf: string[] = ["left", "center", "right"]; diff --git a/src/config/constants/app.ts b/src/config/constants/app.ts new file mode 100644 index 0000000..4ae1ad3 --- /dev/null +++ b/src/config/constants/app.ts @@ -0,0 +1 @@ +export const LANG = "lang"; // 多语言key diff --git a/src/config/constants/filter.ts b/src/config/constants/filter.ts new file mode 100644 index 0000000..ef77d64 --- /dev/null +++ b/src/config/constants/filter.ts @@ -0,0 +1,196 @@ +// UI类型 +export const uiType = { + SELECT: "select", + COLOR: "color", + NUMBER: "number" +}; + +// 有参数滤镜 +export const paramsFilters = [ + { + type: "Brightness", + status: false, + params: [ + { + key: "brightness", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1, + step: 0.01 + } + ] + }, + { + type: "Contrast", + status: false, + params: [ + { + key: "contrast", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1, + step: 0.01 + } + ] + }, + { + type: "Saturation", + status: false, + params: [ + { + key: "saturation", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1, + step: 0.01 + } + ] + }, + { + type: "Vibrance", + status: false, + params: [ + { + key: "vibrance", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1, + step: 0.01 + } + ] + }, + { + type: "HueRotation", + status: false, + params: [ + { + key: "rotation", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1, + step: 0.01 + } + ] + }, + { + type: "Noise", + status: false, + params: [ + { + key: "noise", + value: 0, + uiType: uiType.NUMBER, + min: -1, + max: 1000, + step: 0.1 + } + ] + }, + { + type: "Pixelate", + status: false, + params: [ + { + key: "blocksize", + value: 0.01, + uiType: uiType.NUMBER, + min: 0.01, + max: 100, + step: 0.01 + } + ] + }, + { + type: "Blur", + status: false, + params: [ + { + key: "blur", + value: 0, + uiType: uiType.NUMBER, + min: 0, + max: 1, + step: 0.01 + } + ] + }, + { + type: "Grayscale", + status: false, + params: [ + { + key: "mode", + value: "average", + uiType: uiType.SELECT, + list: ["average", "lightness", "luminosity"] + } + ] + }, + { + type: "RemoveColor", + status: false, + params: [ + { + key: "color", + value: "", + uiType: uiType.COLOR + }, + { + key: "distance", + value: 0, + uiType: uiType.NUMBER, + min: 0, + max: 1, + step: 0.01 + } + ] + } +]; + +// 组合式参数滤镜 +export const combinationFilters = [ + { + type: "Gamma", + status: false, + params: [ + { + key: "red", + value: 0, + uiType: uiType.NUMBER, + min: 0.01, + max: 2.2, + step: 0.01 + }, + { + key: "green", + value: 0, + uiType: uiType.NUMBER, + min: 0.01, + max: 2.2, + step: 0.01 + }, + { + key: "blue", + value: 0, + uiType: uiType.NUMBER, + min: 0.01, + max: 2.2, + step: 0.01 + } + ], + handler( + red: number | string, + green: number | string, + blue: number | string + ) { + return { + gamma: [red, green, blue] + }; + } + } +]; diff --git a/src/core/ContextMenu.js b/src/core/ContextMenu.js new file mode 100644 index 0000000..46041f8 --- /dev/null +++ b/src/core/ContextMenu.js @@ -0,0 +1,270 @@ +/* eslint-disable no-prototype-builtins */ +/* + * @Author: 秦少卫 + * @Date: 2023-05-25 22:33:23 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:31:16 + * @Description: 右键菜单 + */ +class ContextMenu { + constructor(container, items) { + this.container = container; + this.dom = null; + this.shown = false; + this.root = true; + this.parent = null; + this.submenus = []; + this.items = items; + + this._onclick = e => { + if ( + this.dom && + e.target != this.dom && + e.target.parentElement != this.dom && + !e.target.classList.contains("item") && + !e.target.parentElement.classList.contains("item") + ) { + this.hideAll(); + } + }; + + this._oncontextmenu = e => { + e.preventDefault(); + if ( + e.target != this.dom && + e.target.parentElement != this.dom && + !e.target.classList.contains("item") && + !e.target.parentElement.classList.contains("item") + ) { + this.hideAll(); + this.show(e.clientX, e.clientY); + } + }; + + this._oncontextmenu_keydown = e => { + if (e.keyCode != 93) return; + e.preventDefault(); + + this.hideAll(); + this.show(e.clientX, e.clientY); + }; + + this._onblur = () => { + this.hideAll(); + }; + } + + getMenuDom() { + const menu = document.createElement("div"); + menu.classList.add("context"); + + for (const item of this.items) { + menu.appendChild(this.itemToDomEl(item)); + } + + return menu; + } + + itemToDomEl(data) { + const item = document.createElement("div"); + + if (data === null) { + item.classList = "separator"; + return item; + } + + if ( + data.hasOwnProperty("color") && + /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(data.color.toString()) + ) { + item.style.cssText = `color: ${data.color}`; + } + + item.classList.add("item"); + + const label = document.createElement("span"); + label.classList = "label"; + label.innerText = data.hasOwnProperty("text") + ? data["text"].toString() + : ""; + item.appendChild(label); + + if (data.hasOwnProperty("disabled") && data["disabled"]) { + item.classList.add("disabled"); + } else { + item.classList.add("enabled"); + } + + const hotkey = document.createElement("span"); + hotkey.classList = "hotkey"; + hotkey.innerText = data.hasOwnProperty("hotkey") + ? data["hotkey"].toString() + : ""; + item.appendChild(hotkey); + + if ( + data.hasOwnProperty("subitems") && + Array.isArray(data["subitems"]) && + data["subitems"].length > 0 + ) { + const menu = new ContextMenu(this.container, data["subitems"]); + menu.root = false; + menu.parent = this; + + const openSubItems = () => { + if (data.hasOwnProperty("disabled") && data["disabled"] == true) return; + + this.hideSubMenus(); + + const x = this.dom.offsetLeft + this.dom.clientWidth + item.offsetLeft; + const y = this.dom.offsetTop + item.offsetTop; + + if (!menu.shown) { + menu.show(x, y); + } else { + menu.hide(); + } + }; + + this.submenus.push(menu); + + item.classList.add("has-subitems"); + item.addEventListener("click", openSubItems); + item.addEventListener("mousemove", openSubItems); + } else if ( + data.hasOwnProperty("submenu") && + data["submenu"] instanceof ContextMenu + ) { + const menu = data["submenu"]; + menu.root = false; + menu.parent = this; + + const openSubItems = () => { + if (data.hasOwnProperty("disabled") && data["disabled"] == true) return; + + this.hideSubMenus(); + + const x = this.dom.offsetLeft + this.dom.clientWidth + item.offsetLeft; + const y = this.dom.offsetTop + item.offsetTop; + + if (!menu.shown) { + menu.show(x, y); + } else { + menu.hide(); + } + }; + + this.submenus.push(menu); + + item.classList.add("has-subitems"); + item.addEventListener("click", openSubItems); + item.addEventListener("mousemove", openSubItems); + } else { + item.addEventListener("click", () => { + this.hideSubMenus(); + + if (item.classList.contains("disabled")) return; + + if ( + data.hasOwnProperty("onclick") && + typeof data["onclick"] === "function" + ) { + const event = { + handled: false, + item: item, + label: label, + hotkey: hotkey, + items: this.items, + data: data + }; + + data["onclick"](event); + + if (!event.handled) { + this.hide(); + } + } else { + this.hide(); + } + }); + + item.addEventListener("mousemove", () => { + this.hideSubMenus(); + }); + } + + return item; + } + + hideAll() { + if (this.root && !this.parent) { + if (this.shown) { + this.hideSubMenus(); + + this.shown = false; + this.container.removeChild(this.dom); + + if (this.parent && this.parent.shown) { + this.parent.hide(); + } + } + + return; + } + + this.parent.hide(); + } + + hide() { + if (this.dom && this.shown) { + this.shown = false; + this.hideSubMenus(); + this.container.removeChild(this.dom); + + if (this.parent && this.parent.shown) { + this.parent.hide(); + } + } + } + + hideSubMenus() { + for (const menu of this.submenus) { + if (menu.shown) { + menu.shown = false; + menu.container.removeChild(menu.dom); + } + menu.hideSubMenus(); + } + } + + show(x, y) { + this.dom = this.getMenuDom(); + + this.dom.style.left = `${x}px`; + this.dom.style.top = `${y}px`; + + this.shown = true; + this.container.appendChild(this.dom); + } + + install() { + this.container.addEventListener("contextmenu", this._oncontextmenu); + this.container.addEventListener("keydown", this._oncontextmenu_keydown); + this.container.addEventListener("click", this._onclick); + window.addEventListener("blur", this._onblur); + } + + setData(data) { + this.items = data; + } + + uninstall() { + this.dom = null; + // this.container.removeEventListener('contextmenu', this._oncontextmenu); + this.container.removeEventListener("keydown", this._oncontextmenu_keydown); + this.container.removeEventListener("click", this._onclick); + window.removeEventListener("blur", this._onblur); + } +} + +export default ContextMenu; diff --git a/src/core/ServersPlugin.ts b/src/core/ServersPlugin.ts new file mode 100644 index 0000000..d7a7272 --- /dev/null +++ b/src/core/ServersPlugin.ts @@ -0,0 +1,258 @@ +/* + * @Author: zhoux zhouxia@supervision.ltd + * @Date: 2023-11-29 09:31:35 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:12:48 + * @FilePath: \vue-fabric-editor\src\core\ServersPlugin.ts + * @Description: 内部插件 + */ + +import { v4 as uuid } from "uuid"; +import { selectFiles, clipboardText } from "@/utils/utils"; +// import { clipboardText } from '@/utils/utils.ts'; +import { fabric } from "fabric"; +import Editor from "../core"; +import { customJsonAttr } from "@/hooks/config"; +type IEditor = Editor; +// import { v4 as uuid } from 'uuid'; + +function downFile(fileStr: string, fileType: string) { + const anchorEl = document.createElement("a"); + anchorEl.href = fileStr; + anchorEl.download = `${uuid()}.${fileType}`; + document.body.appendChild(anchorEl); // required for firefox + anchorEl.click(); + anchorEl.remove(); +} + +function transformText(objects) { + if (!objects) return; + objects.forEach(item => { + if (item.objects) { + transformText(item.objects); + } else { + item.type === "text" && (item.type = "textbox"); + } + }); +} + +class ServersPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "ServersPlugin"; + public elementHandler: ElementHandler; + + static apis = [ + "insert", + "insertSvgFile", + "getJson", + "dragAddItem", + "clipboard", + "saveJson", + "saveSvg", + "saveImg", + "clear", + "preview" + ]; + // public hotkeys: string[] = ['left', 'right', 'down', 'up']; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + insert() { + selectFiles({ accept: ".json" }).then(files => { + const [file] = files; + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = () => { + this.insertSvgFile(reader.result); + }; + }); + } + + insertSvgFile(jsonFile) { + console.log(jsonFile, "insertSvgFile"); + // 加载前钩子 + this.editor.hooksEntity.hookImportBefore.callAsync(jsonFile, () => { + this.canvas.loadFromJSON(jsonFile, () => { + this.canvas.renderAll(); + // 加载后钩子 + this.editor.hooksEntity.hookImportAfter.callAsync(jsonFile, () => { + this.canvas.renderAll(); + }); + }); + }); + } + + /** + * Set partial by object + * @param {FabricObject} obj + * @param {FabricObjectOption} option + * @returns + */ + // setByPartial(obj: FabricObject, option: FabricObjectOption) { + // if (!obj) { + // return; + // } + // if (obj.type === 'svg') { + // if (option.fill) { + // obj.setFill(option.fill); + // } else if (option.stroke) { + // obj.setStroke(option.stroke); + // } + // } + // obj.set(option); + // obj.setCoords(); + // this.canvas.renderAll(); + // const { id, superType, type, player, width, height } = obj as any; + // if (superType === 'element') { + // if ('visible' in option) { + // if (option.visible) { + // obj.element.style.display = 'block'; + // } else { + // obj.element.style.display = 'none'; + // } + // } + // const el = this.elementHandler.findById(id); + // // update the element + // this.elementHandler.setScaleOrAngle(el, obj); + // this.elementHandler.setSize(el, obj); + // this.elementHandler.setPosition(el, obj); + // if (type === 'video' && player) { + // player.setPlayerSize(width, height); + // } + // } + // } + + getJson() { + // update 在json对象中新增的属性需要在此备注 + return this.canvas.toJSON([...customJsonAttr]); + } + + /** + * @description: 拖拽添加到画布 + * @param {Event} event + * @param {Object} item + */ + dragAddItem(event: DragEvent, item: fabric.Object) { + const { left, top } = this.canvas + .getSelectionElement() + .getBoundingClientRect(); + if (event.x < left || event.y < top || item.width === undefined) return; + + const point = { + x: event.x - left, + y: event.y - top + }; + const pointerVpt = this.canvas.restorePointerVpt(point); + item.left = pointerVpt.x - item.width / 2; + item.top = pointerVpt.y; + this.canvas.add(item); + this.canvas.requestRenderAll(); + } + + clipboard() { + const jsonStr = this.getJson(); + clipboardText(JSON.stringify(jsonStr, null, "\t")); + } + + async saveJson() { + const dataUrl = this.getJson(); + console.log(dataUrl, "saveJson_dataUrl"); + // 把文本text转为textgroup,让导入可以编辑 + await transformText(dataUrl.objects); + const fileStr = `data:text/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ ...dataUrl }, null, "\t") + )}`; + downFile(fileStr, "json"); + } + + saveSvg() { + this.editor.hooksEntity.hookSaveBefore.callAsync("", () => { + const option = this._getSaveSvgOption(); + const dataUrl = this.canvas.toSVG(option); + const fileStr = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( + dataUrl + )}`; + this.editor.hooksEntity.hookSaveAfter.callAsync(fileStr, () => { + downFile(fileStr, "svg"); + }); + }); + } + + saveImg() { + this.editor.hooksEntity.hookSaveBefore.callAsync("", () => { + const option = this._getSaveOption(); + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + const dataUrl = this.canvas.toDataURL(option); + this.editor.hooksEntity.hookSaveAfter.callAsync(dataUrl, () => { + downFile(dataUrl, "png"); + }); + }); + } + + preview() { + return new Promise((resolve, _) => { + this.editor.hooksEntity.hookSaveBefore.callAsync("", () => { + const option = this._getSaveOption(); + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + this.canvas.renderAll(); + const dataUrl = this.canvas.toDataURL(option); + this.editor.hooksEntity.hookSaveAfter.callAsync(dataUrl, () => { + resolve(dataUrl); + }); + }); + }); + } + + _getSaveSvgOption() { + const workspace = this.canvas + .getObjects() + .find(item => item.id === "workspace"); + const { left, top, width, height } = workspace; + return { + width, + height, + viewBox: { + x: left, + y: top, + width, + height + } + }; + } + + _getSaveOption() { + const workspace = this.canvas + .getObjects() + .find((item: fabric.Object) => item.id === "workspace"); + const { left, top, width, height } = workspace as fabric.Object; + const option = { + name: "New Image", + format: "png", + quality: 1, + width, + height, + left, + top + }; + return option; + } + + clear() { + this.canvas.getObjects().forEach(obj => { + if (obj.id !== "workspace") { + this.canvas.remove(obj); + } + }); + this.canvas.discardActiveObject(); + this.canvas.renderAll(); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default ServersPlugin; diff --git a/src/core/core.ts b/src/core/core.ts new file mode 100644 index 0000000..60da8ec --- /dev/null +++ b/src/core/core.ts @@ -0,0 +1,169 @@ +import EventEmitter from "events"; +import hotkeys from "hotkeys-js"; +import ContextMenu from "./ContextMenu.js"; +import ServersPlugin from "./ServersPlugin"; +import { AsyncSeriesHook } from "tapable"; + +class Editor extends EventEmitter { + canvas: fabric.Canvas; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + contextMenu: any; + private pluginMap: { + [propName: string]: IPluginTempl; + } = {}; + // 自定义事件 + private customEvents: string[] = []; + // 自定义API + private customApis: string[] = []; + // 生命周期函数名 + private hooks: IEditorHooksType[] = [ + "hookImportBefore", + "hookImportAfter", + "hookSaveBefore", + "hookSaveAfter" + ]; + private hooksEntity: { + [propName: string]: AsyncSeriesHook; + } = {}; + // constructor(canvas: fabric.Canvas) { + // super(); + // this.canvas = canvas; + // this._initContextMenu(); + // this._bindContextMenu(); + // this._initActionHooks(); + // this._initServersPlugin(); + // } + init(canvas: fabric.Canvas) { + this.canvas = canvas; + this._initContextMenu(); + this._bindContextMenu(); + this._initActionHooks(); + this._initServersPlugin(); + } + + // 引入组件 + use(plugin: IPluginClass, options: IPluginOption) { + if (this._checkPlugin(plugin)) { + this._saveCustomAttr(plugin); + const pluginRunTime = new plugin(this.canvas, this, options); + this.pluginMap[plugin.pluginName] = pluginRunTime; + this._bindingHooks(pluginRunTime); + this._bindingHotkeys(pluginRunTime); + this._bindingApis(pluginRunTime); + } + } + + // 获取插件 + getPlugin(name: string) { + if (this.pluginMap[name]) { + return this.pluginMap[name]; + } + } + + // 检查组件 + private _checkPlugin(plugin: IPluginClass) { + const { pluginName, events = [], apis = [] } = plugin; + //名称检查 + if (this.pluginMap[pluginName]) { + throw new Error(pluginName + "插件重复初始化"); + } + events.forEach((eventName: string) => { + if (this.customEvents.find(info => info === eventName)) { + throw new Error(pluginName + "插件中" + eventName + "重复"); + } + }); + + apis.forEach((apiName: string) => { + if (this.customApis.find(info => info === apiName)) { + throw new Error(pluginName + "插件中" + apiName + "重复"); + } + }); + return true; + } + + // 绑定hooks方法 + private _bindingHooks(plugin: IPluginTempl) { + this.hooks.forEach(hookName => { + const hook = plugin[hookName]; + if (hook) { + this.hooksEntity[hookName].tapPromise( + plugin.pluginName + hookName, + function () { + // eslint-disable-next-line prefer-rest-params + return hook.apply(plugin, [...arguments]); + } + ); + } + }); + } + + // 绑定快捷键 + private _bindingHotkeys(plugin: IPluginTempl) { + plugin?.hotkeys?.forEach((keyName: string) => { + // 支持 keyup + hotkeys(keyName, { keyup: true }, e => plugin.hotkeyEvent(keyName, e)); + }); + } + + // 保存组件自定义事件与API + private _saveCustomAttr(plugin: IPluginClass) { + const { events = [], apis = [] } = plugin; + this.customApis = this.customApis.concat(apis); + this.customEvents = this.customEvents.concat(events); + } + // 代理API事件 + private _bindingApis(pluginRunTime: IPluginTempl) { + const { apis = [] } = pluginRunTime.constructor; + apis.forEach(apiName => { + this[apiName] = function () { + // eslint-disable-next-line prefer-rest-params + return pluginRunTime[apiName].apply(pluginRunTime, [...arguments]); + }; + }); + } + + // 右键菜单 + private _bindContextMenu() { + this.canvas.on("mouse:down", opt => { + if (opt.button === 3) { + let menu: IPluginMenu[] = []; + Object.keys(this.pluginMap).forEach(pluginName => { + const pluginRunTime = this.pluginMap[pluginName]; + const pluginMenu = + pluginRunTime.contextMenu && pluginRunTime.contextMenu(); + if (pluginMenu) { + menu = menu.concat(pluginMenu); + } + }); + this._renderMenu(opt, menu); + } + }); + } + + // 渲染右键菜单 + private _renderMenu(opt: fabric.IEvent, menu: IPluginMenu[]) { + if (menu.length !== 0) { + this.contextMenu.hideAll(); + this.contextMenu.setData(menu); + this.contextMenu.show(opt.e.clientX, opt.e.clientY); + } + } + + // 生命周期事件 + _initActionHooks() { + this.hooks.forEach(hookName => { + this.hooksEntity[hookName] = new AsyncSeriesHook(["data"]); + }); + } + + _initContextMenu() { + this.contextMenu = new ContextMenu(this.canvas.wrapperEl, []); + this.contextMenu.install(); + } + + _initServersPlugin() { + this.use(ServersPlugin, {}); + } +} + +export default Editor; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..b3ab46b --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,50 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-02-03 23:29:34 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:13:16 + * @Description: 核心入口文件 + */ +import Editor from "./core"; +import DringPlugin from "./plugin/DringPlugin"; +import AlignGuidLinePlugin from "./plugin/AlignGuidLinePlugin"; +import ControlsPlugin from "./plugin/ControlsPlugin"; +import ControlsRotatePlugin from "./plugin/ControlsRotatePlugin"; +import CenterAlignPlugin from "./plugin/CenterAlignPlugin"; +import LayerPlugin from "./plugin/LayerPlugin"; +import CopyPlugin from "./plugin/CopyPlugin"; +import MoveHotKeyPlugin from "./plugin/MoveHotKeyPlugin"; +import DeleteHotKeyPlugin from "./plugin/DeleteHotKeyPlugin"; +import GroupPlugin from "./plugin/GroupPlugin"; +import DrawLinePlugin from "./plugin/DrawLinePlugin"; +import GroupTextEditorPlugin from "./plugin/GroupTextEditorPlugin"; +import GroupAlignPlugin from "./plugin/GroupAlignPlugin"; +import WorkspacePlugin from "./plugin/WorkspacePlugin"; +import DownFontPlugin from "./plugin/DownFontPlugin"; +import HistoryPlugin from "./plugin/HistoryPlugin"; +import FlipPlugin from "./plugin/FlipPlugin"; +import RulerPlugin from "./plugin/RulerPlugin"; +import MaterialPlugin from "./plugin/MaterialPlugin"; + +export { + DringPlugin, + AlignGuidLinePlugin, + ControlsPlugin, + ControlsRotatePlugin, + CenterAlignPlugin, + LayerPlugin, + CopyPlugin, + MoveHotKeyPlugin, + DeleteHotKeyPlugin, + GroupPlugin, + DrawLinePlugin, + GroupTextEditorPlugin, + GroupAlignPlugin, + WorkspacePlugin, + DownFontPlugin, + HistoryPlugin, + FlipPlugin, + RulerPlugin, + MaterialPlugin +}; +export default Editor; diff --git a/src/core/objects/Arrow.ts b/src/core/objects/Arrow.ts new file mode 100644 index 0000000..aa7d464 --- /dev/null +++ b/src/core/objects/Arrow.ts @@ -0,0 +1,46 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-01-07 01:15:50 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:12:08 + * @Description: 箭头元素 + */ +import { fabric } from "fabric"; + +fabric.Arrow = fabric.util.createClass(fabric.Line, { + type: "arrow", + superType: "drawing", + initialize(points, options) { + if (!points) { + const { x1, x2, y1, y2 } = options; + points = [x1, y1, x2, y2]; + } + options = options || {}; + this.callSuper("initialize", points, options); + }, + _render(ctx) { + this.callSuper("_render", ctx); + ctx.save(); + const xDiff = this.x2 - this.x1; + const yDiff = this.y2 - this.y1; + const angle = Math.atan2(yDiff, xDiff); + ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2); + ctx.rotate(angle); + ctx.beginPath(); + // Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0) + ctx.moveTo(5, 0); + ctx.lineTo(-5, 5); + ctx.lineTo(-5, -5); + ctx.closePath(); + ctx.fillStyle = this.stroke; + ctx.fill(); + ctx.restore(); + } +}); + +fabric.Arrow.fromObject = (options, callback) => { + const { x1, x2, y1, y2 } = options; + return callback(new fabric.Arrow([x1, y1, x2, y2], options)); +}; + +export default fabric.Arrow; diff --git a/src/core/plugin.ts b/src/core/plugin.ts new file mode 100644 index 0000000..095945f --- /dev/null +++ b/src/core/plugin.ts @@ -0,0 +1,101 @@ +/* + * @Author: donghao donghao@supervision.ltd + * @Date: 2024-08-06 17:11:20 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:13:06 + * @FilePath: \General-AI-Platform-Web-Client\src\core\plugin.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +import Editor from "./core"; +type IEditor = Editor; + +class EditorWorkspacePlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + public defautOption = { + color: "red", + size: 0.5 + }; + static pluginName = "textPlugin"; + static events = ["textEvent1", "textEvent2"]; + static apis = ["textAPI1", "textAPI2"]; + public hotkeys: string[] = ["ctrl+v", "ctrl+a"]; + constructor( + canvas: fabric.Canvas, + editor: IEditor, + options: IPluginOption = {} + ) { + this.canvas = canvas; + this.editor = editor; + this.defautOption = { ...this.defautOption, ...options }; + this.init(); + } + init() { + console.log("pluginInit", this.canvas, this.editor, this.defautOption); + } + + destroy() { + console.log("pluginDestroy"); + } + // 保存文件前 + hookSaveBefore() { + console.log("pluginHookSaveBefore"); + } + // 保存文件前 + hookSaveAfter() { + console.log("pluginHookSaveAfter"); + } + // 快捷键扩展回调 + hotkeyEvent(eventName: string, e?: Event) { + console.log("pluginHotkeyEvent", eventName, e); + } + // 右键菜单扩展 + contextMenu() { + return [ + { text: "Back", hotkey: "Alt+Left arrow", disabled: true }, + { text: "Forward", hotkey: "Alt+Right arrow", disabled: true }, + { text: "Reload", hotkey: "Ctrl+R" }, + null, + { text: "Save as...", hotkey: "Ctrl+S" }, + { text: "Print...", hotkey: "Ctrl+P" }, + { text: "Cast..." }, + { text: "Translate to English" }, + null, + { text: "View page source", hotkey: "Ctrl+U" }, + { text: "Inspect", hotkey: "Ctrl+Shift+I" }, + null, + { + text: "Kali tools", + hotkey: "❯", + subitems: [ + { + text: "Fuzzing Tools", + hotkey: "❯", + subitems: [ + { text: "spike-generic_chunked" }, + { text: "spike-generic_listen_tcp" }, + { text: "spike-generic_send_tcp" }, + { text: "spike-generic_send_udp" } + ] + }, + { + text: "VoIP Tools", + hotkey: "❯", + subitems: [{ text: "voiphopper" }] + }, + { text: "nikto" }, + { text: "nmap" }, + { text: "sparta" }, + { text: "unix-privesc-check" } + ] + }, + { text: "Skins", hotkey: "❯" } + ]; + } + + _command() { + console.log("pluginContextMenuCommand"); + } +} + +export default EditorWorkspacePlugin; diff --git a/src/core/plugin/AlignGuidLinePlugin.ts b/src/core/plugin/AlignGuidLinePlugin.ts new file mode 100644 index 0000000..88c0ecd --- /dev/null +++ b/src/core/plugin/AlignGuidLinePlugin.ts @@ -0,0 +1,357 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-05-21 08:55:25 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:13:57 + * @Description: 辅助线功能 + */ + +import Editor from "../core"; +type IEditor = Editor; + +import { fabric } from "fabric"; + +declare interface VerticalLine { + x: number; + y1: number; + y2: number; +} + +declare interface HorizontalLine { + x1: number; + x2: number; + y: number; +} + +class AlignGuidLinePlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + public defautOption = { + color: "rgba(255,95,95,1)", + width: 1 + }; + static pluginName = "AlignGuidLinePlugin"; + static events = ["", ""]; + static apis = []; + public hotkeys: string[] = [""]; + dragMode = false; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.dragMode = false; + this.init(); + } + init() { + const { canvas } = this; + const ctx = canvas.getSelectionContext(); + const aligningLineOffset = 5; + const aligningLineMargin = 4; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + let viewportTransform: number[] | undefined; + let zoom = 1; + + function drawVerticalLine(coords: VerticalLine) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1 + ); + } + + function drawHorizontalLine(coords: HorizontalLine) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5 + ); + } + + function drawLine(x1: number, y1: number, x2: number, y2: number) { + if (viewportTransform == null) return; + + ctx.save(); + ctx.lineWidth = self.defautOption.width; + ctx.strokeStyle = self.defautOption.color; + ctx.beginPath(); + ctx.moveTo( + x1 * zoom + viewportTransform[4], + y1 * zoom + viewportTransform[5] + ); + ctx.lineTo( + x2 * zoom + viewportTransform[4], + y2 * zoom + viewportTransform[5] + ); + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1: number, value2: number) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for ( + let i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; + i <= len; + i++ + ) { + if (i === value2) { + return true; + } + } + return false; + } + + const verticalLines: VerticalLine[] = []; + const horizontalLines: HorizontalLine[] = []; + + canvas.on("mouse:down", () => { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on("object:moving", e => { + if (viewportTransform === undefined || e.target === undefined) return; + + const activeObject = e.target; + const canvasObjects = canvas.getObjects(); + const activeObjectCenter = activeObject.getCenterPoint(); + const activeObjectLeft = activeObjectCenter.x; + const activeObjectTop = activeObjectCenter.y; + const activeObjectBoundingRect = activeObject.getBoundingRect(); + const activeObjectHeight = + activeObjectBoundingRect.height / viewportTransform[3]; + const activeObjectWidth = + activeObjectBoundingRect.width / viewportTransform[0]; + let horizontalInTheRange = false; + let verticalInTheRange = false; + const transform = canvas._currentTransform; + + if (!transform) return; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (let i = canvasObjects.length; i--; ) { + // eslint-disable-next-line no-continue + if (canvasObjects[i] === activeObject) continue; + + // 排除辅助线 + if ( + activeObject instanceof fabric.GuideLine && + canvasObjects[i] instanceof fabric.GuideLine + ) { + continue; + } + + const objectCenter = canvasObjects[i].getCenterPoint(); + const objectLeft = objectCenter.x; + const objectTop = objectCenter.y; + const objectBoundingRect = canvasObjects[i].getBoundingRect(); + const objectHeight = objectBoundingRect.height / viewportTransform[3]; + const objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point(objectLeft, activeObjectTop), + "center", + "center" + ); + } + + // snap by the left edge + if ( + isInRange( + objectLeft - objectWidth / 2, + activeObjectLeft - activeObjectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft - objectWidth / 2 + activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snap by the right edge + if ( + isInRange( + objectLeft + objectWidth / 2, + activeObjectLeft + activeObjectWidth / 2 + ) + ) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: + objectTop < activeObjectTop + ? objectTop - objectHeight / 2 - aligningLineOffset + : objectTop + objectHeight / 2 + aligningLineOffset, + y2: + activeObjectTop > objectTop + ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset + : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point( + objectLeft + objectWidth / 2 - activeObjectWidth / 2, + activeObjectTop + ), + "center", + "center" + ); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point(activeObjectLeft, objectTop), + "center", + "center" + ); + } + + // snap by the top edge + if ( + isInRange( + objectTop - objectHeight / 2, + activeObjectTop - activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop - objectHeight / 2 + activeObjectHeight / 2 + ), + "center", + "center" + ); + } + + // snap by the bottom edge + if ( + isInRange( + objectTop + objectHeight / 2, + activeObjectTop + activeObjectHeight / 2 + ) + ) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: + objectLeft < activeObjectLeft + ? objectLeft - objectWidth / 2 - aligningLineOffset + : objectLeft + objectWidth / 2 + aligningLineOffset, + x2: + activeObjectLeft > objectLeft + ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset + : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset + }); + activeObject.setPositionByOrigin( + new fabric.Point( + activeObjectLeft, + objectTop + objectHeight / 2 - activeObjectHeight / 2 + ), + "center", + "center" + ); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on("before:render", () => { + // fix 保存图片时报错 + try { + canvas.clearContext(canvas.contextTop); + } catch (error) { + console.log(error); + } + }); + + canvas.on("after:render", () => { + for (let i = verticalLines.length; i--; ) { + drawVerticalLine(verticalLines[i]); + } + for (let j = horizontalLines.length; j--; ) { + drawHorizontalLine(horizontalLines[j]); + } + + // noinspection NestedAssignmentJS + verticalLines.length = 0; + horizontalLines.length = 0; + }); + + canvas.on("mouse:up", () => { + verticalLines.length = 0; + horizontalLines.length = 0; + canvas.renderAll(); + }); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default AlignGuidLinePlugin; diff --git a/src/core/plugin/CenterAlignPlugin.ts b/src/core/plugin/CenterAlignPlugin.ts new file mode 100644 index 0000000..f134684 --- /dev/null +++ b/src/core/plugin/CenterAlignPlugin.ts @@ -0,0 +1,76 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-15 22:49:42 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:14:20 + * @Description: 居中对齐插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; + +class CenterAlignPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "CenterAlignPlugin"; + static apis = ["centerH", "center", "position", "centerV"]; + // public hotkeys: string[] = ['space']; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + center(workspace: fabric.Rect, object: fabric.Object) { + const center = workspace.getCenterPoint(); + return this.canvas._centerObject(object, center); + } + + centerV(workspace: fabric.Rect, object: fabric.Object) { + return this.canvas._centerObject( + object, + new fabric.Point(object.getCenterPoint().x, workspace.getCenterPoint().y) + ); + } + + centerH(workspace: fabric.Rect, object: fabric.Object) { + return this.canvas._centerObject( + object, + new fabric.Point(workspace.getCenterPoint().x, object.getCenterPoint().y) + ); + } + + position(name: "centerH" | "center" | "centerV") { + const anignType = ["centerH", "center", "centerV"]; + const activeObject = this.canvas.getActiveObject(); + if (anignType.includes(name) && activeObject) { + const defaultWorkspace = this.canvas + .getObjects() + .find(item => item.id === "workspace"); + if (defaultWorkspace) { + console.log(this[name]); + this[name](defaultWorkspace, activeObject); + } + this.canvas.renderAll(); + } + } + + contextMenu() { + const activeObject = this.canvas.getActiveObject(); + if (activeObject) { + return [ + { + text: "水平垂直居中", + hotkey: "Ctrl+V", + disabled: false, + onclick: () => this.position("center") + } + ]; + } + } + destroy() { + console.log("pluginDestroy"); + } +} + +export default CenterAlignPlugin; diff --git a/src/core/plugin/ControlsPlugin.ts b/src/core/plugin/ControlsPlugin.ts new file mode 100644 index 0000000..13df367 --- /dev/null +++ b/src/core/plugin/ControlsPlugin.ts @@ -0,0 +1,262 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-13 23:00:43 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-07 17:17:54 + * @Description: 控制条插件 + */ + +import Editor from "../core"; +type IEditor = Editor; + +import { fabric } from "fabric"; +import verticalImg from "@/assets/editor/middlecontrol.svg"; +import horizontalImg from "@/assets/editor/middlecontrolhoz.svg"; +import edgeImg from "@/assets/editor/edgecontrol.svg"; +import rotateImg from "@/assets/editor/rotateicon.svg"; + +/** + * 实际场景: 在进行某个对象缩放的时候,由于fabricjs默认精度使用的是toFixed(2)。 + * 此处为了缩放的精度更准确一些,因此将NUM_FRACTION_DIGITS默认值改为4,即toFixed(4). + */ +fabric.Object.NUM_FRACTION_DIGITS = 4; + +function drawImg( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + img: HTMLImageElement, + wSize: number, + hSize: number, + angle: number | undefined +) { + if (angle === undefined) return; + ctx.save(); + ctx.translate(left, top); + ctx.rotate(fabric.util.degreesToRadians(angle)); + console.log(img, -wSize / 2, -hSize / 2, wSize, hSize, "drawImg"); + ctx.drawImage(img, -wSize / 2, -hSize / 2, wSize, hSize); + ctx.restore(); +} + +// 中间横杠 +function intervalControl() { + const verticalImgIcon = document.createElement("img"); + verticalImgIcon.src = verticalImg; + + const horizontalImgIcon = document.createElement("img"); + horizontalImgIcon.src = horizontalImg; + + function renderIcon( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object + ) { + drawImg(ctx, left, top, verticalImgIcon, 20, 25, fabricObject.angle); + } + + function renderIconHoz( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object + ) { + drawImg(ctx, left, top, horizontalImgIcon, 25, 20, fabricObject.angle); + } + // 中间横杠 + fabric.Object.prototype.controls.ml = new fabric.Control({ + x: -0.5, + y: 0, + offsetX: -1, + cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingXOrSkewingY, + getActionName: fabric.controlsUtils.scaleOrSkewActionName, + render: renderIcon + }); + + fabric.Object.prototype.controls.mr = new fabric.Control({ + x: 0.5, + y: 0, + offsetX: 1, + cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingXOrSkewingY, + getActionName: fabric.controlsUtils.scaleOrSkewActionName, + render: renderIcon + }); + + fabric.Object.prototype.controls.mb = new fabric.Control({ + x: 0, + y: 0.5, + offsetY: 1, + cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingYOrSkewingX, + getActionName: fabric.controlsUtils.scaleOrSkewActionName, + render: renderIconHoz + }); + + fabric.Object.prototype.controls.mt = new fabric.Control({ + x: 0, + y: -0.5, + offsetY: -1, + cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingYOrSkewingX, + getActionName: fabric.controlsUtils.scaleOrSkewActionName, + render: renderIconHoz + }); +} + +// 顶点 +function peakControl() { + const img = document.createElement("img"); + img.src = edgeImg; + + function renderIconEdge( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object + ) { + drawImg(ctx, left, top, img, 25, 25, fabricObject.angle); + } + // 四角图标 + fabric.Object.prototype.controls.tl = new fabric.Control({ + x: -0.5, + y: -0.5, + cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingEqually, + render: renderIconEdge + }); + fabric.Object.prototype.controls.bl = new fabric.Control({ + x: -0.5, + y: 0.5, + cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingEqually, + render: renderIconEdge + }); + fabric.Object.prototype.controls.tr = new fabric.Control({ + x: 0.5, + y: -0.5, + cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingEqually, + render: renderIconEdge + }); + fabric.Object.prototype.controls.br = new fabric.Control({ + x: 0.5, + y: 0.5, + cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler, + actionHandler: fabric.controlsUtils.scalingEqually, + render: renderIconEdge + }); +} +// 删除 +function deleteControl(canvas: fabric.Canvas) { + const deleteIcon = + "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + const delImg = document.createElement("img"); + delImg.src = deleteIcon; + + function renderDelIcon( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object + ) { + drawImg(ctx, left, top, delImg, 24, 24, fabricObject.angle); + } + + // 删除选中元素 + function deleteObject(mouseEvent: MouseEvent, target: fabric.Transform) { + if (target.action === "rotate") return true; + const activeObject = canvas.getActiveObjects(); + if (activeObject) { + activeObject.map(item => canvas.remove(item)); + canvas.requestRenderAll(); + canvas.discardActiveObject(); + } + return true; + } + + // 删除图标 + fabric.Object.prototype.controls.deleteControl = new fabric.Control({ + x: 0.5, + y: -0.5, + offsetY: -16, + offsetX: 16, + cursorStyle: "pointer", + mouseUpHandler: deleteObject, + render: renderDelIcon + // cornerSize: 24, + }); +} + +// 旋转 +function rotationControl() { + const img = document.createElement("img"); + img.src = rotateImg; + function renderIconRotate( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object + ) { + drawImg(ctx, left, top, img, 40, 40, fabricObject.angle); + } + // 旋转图标 + fabric.Object.prototype.controls.mtr = new fabric.Control({ + x: 0, + y: 0.5, + cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler, + actionHandler: fabric.controlsUtils.rotationWithSnapping, + offsetY: 30, + // withConnecton: false, + actionName: "rotate", + render: renderIconRotate + }); +} + +class ControlsPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "ControlsPlugin"; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.init(); + } + init() { + // 删除图标 + deleteControl(this.canvas); + // TODO svg引入存在问题,暂时注释 + // 顶点图标 + // peakControl(); + // 中间横杠图标 + // intervalControl(); + // 旋转图标 + // rotationControl(); + + // 选中样式 + fabric.Object.prototype.set({ + transparentCorners: false, + borderColor: "#51B9F9", + cornerColor: "#FFF", + borderScaleFactor: 2.5, + cornerStyle: "circle", + cornerStrokeColor: "#0E98FC", + borderOpacityWhenMoving: 1 + }); + // textbox保持一致 + // fabric.Textbox.prototype.controls = fabric.Object.prototype.controls; + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default ControlsPlugin; diff --git a/src/core/plugin/ControlsRotatePlugin.ts b/src/core/plugin/ControlsRotatePlugin.ts new file mode 100644 index 0000000..da05959 --- /dev/null +++ b/src/core/plugin/ControlsRotatePlugin.ts @@ -0,0 +1,122 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-13 23:07:04 + * @LastEditors: 秦少卫 + * @LastEditTime: 2023-06-13 23:10:52 + * @Description: 控制条插件 + */ + +import Editor from "../core"; +type IEditor = Editor; + +// 定义旋转光标样式,根据转动角度设定光标旋转 +function rotateIcon(angle: number) { + return `url("data:image/svg+xml,%3Csvg height='18' width='18' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'%3E%3Cg fill='none' transform='rotate(${angle} 16 16)'%3E%3Cpath d='M22.4484 0L32 9.57891L22.4484 19.1478V13.1032C17.6121 13.8563 13.7935 17.6618 13.0479 22.4914H19.2141L9.60201 32.01L0 22.4813H6.54912C7.36524 14.1073 14.0453 7.44023 22.4484 6.61688V0Z' fill='white'/%3E%3Cpath d='M24.0605 3.89587L29.7229 9.57896L24.0605 15.252V11.3562C17.0479 11.4365 11.3753 17.0895 11.3048 24.0879H15.3048L9.60201 29.7308L3.90932 24.0879H8.0806C8.14106 15.3223 15.2645 8.22345 24.0605 8.14313V3.89587Z' fill='black'/%3E%3C/g%3E%3C/svg%3E ") 12 12,crosshair`; +} + +class ControlsRotatePlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "ControlsRotatePlugin"; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.init(); + } + init() { + const { canvas } = this; + // 添加旋转控制响应区域 + fabric.Object.prototype.controls.mtr = new fabric.Control({ + x: -0.5, + y: -0.5, + offsetY: -10, + offsetX: -10, + rotate: 20, + actionName: "rotate", + actionHandler: fabric.controlsUtils.rotationWithSnapping, + render: () => "" + }); + // ↖左上 + fabric.Object.prototype.controls.mtr2 = new fabric.Control({ + x: 0.5, + y: -0.5, + offsetY: -10, + offsetX: 10, + rotate: 20, + actionName: "rotate", + actionHandler: fabric.controlsUtils.rotationWithSnapping, + render: () => "" + }); // ↗右上 + fabric.Object.prototype.controls.mtr3 = new fabric.Control({ + x: 0.5, + y: 0.5, + offsetY: 10, + offsetX: 10, + rotate: 20, + actionName: "rotate", + actionHandler: fabric.controlsUtils.rotationWithSnapping, + render: () => "" + }); // ↘右下 + fabric.Object.prototype.controls.mtr4 = new fabric.Control({ + x: -0.5, + y: 0.5, + offsetY: 10, + offsetX: -10, + rotate: 20, + actionName: "rotate", + actionHandler: fabric.controlsUtils.rotationWithSnapping, + render: () => "" + }); // ↙左下 + + // 渲染时,执行 + canvas.on("after:render", () => { + const activeObj = canvas.getActiveObject(); + const angle = activeObj?.angle?.toFixed(2); + if (angle !== undefined) { + fabric.Object.prototype.controls.mtr.cursorStyle = rotateIcon( + Number(angle) + ); + fabric.Object.prototype.controls.mtr2.cursorStyle = rotateIcon( + Number(angle) + 90 + ); + fabric.Object.prototype.controls.mtr3.cursorStyle = rotateIcon( + Number(angle) + 180 + ); + fabric.Object.prototype.controls.mtr4.cursorStyle = rotateIcon( + Number(angle) + 270 + ); + } + }); + + // 旋转时,实时更新旋转控制图标 + canvas.on("object:rotating", event => { + const body = canvas.lowerCanvasEl.nextSibling as HTMLElement; + const angle = canvas.getActiveObject()?.angle?.toFixed(2); + if (angle === undefined) return; + switch (event.transform?.corner) { + case "mtr": + body.style.cursor = rotateIcon(Number(angle)); + break; + case "mtr2": + body.style.cursor = rotateIcon(Number(angle) + 90); + break; + case "mtr3": + body.style.cursor = rotateIcon(Number(angle) + 180); + break; + case "mtr4": + body.style.cursor = rotateIcon(Number(angle) + 270); + break; + default: + break; + } // 设置四角旋转光标 + }); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default ControlsRotatePlugin; + +import { fabric } from "fabric"; diff --git a/src/core/plugin/CopyPlugin.ts b/src/core/plugin/CopyPlugin.ts new file mode 100644 index 0000000..9e43762 --- /dev/null +++ b/src/core/plugin/CopyPlugin.ts @@ -0,0 +1,122 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-20 12:38:37 + * @LastEditors: 秦少卫 + * @LastEditTime: 2023-06-20 13:34:21 + * @Description: 复制插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; +import { v4 as uuid } from "uuid"; + +class CopyPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "CopyPlugin"; + static apis = ["clone"]; + public hotkeys: string[] = ["ctrl+v", "ctrl+c"]; + private cache: null | fabric.ActiveSelection | fabric.Object; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.cache = null; + } + + // 多选对象复制 + _copyActiveSelection(activeObject: fabric.Object) { + // 间距设置 + const grid = 10; + const canvas = this.canvas; + activeObject?.clone((cloned: fabric.Object) => { + // 再次进行克隆,处理选择多个对象的情况 + cloned.clone((clonedObj: fabric.ActiveSelection) => { + canvas.discardActiveObject(); + if (clonedObj.left === undefined || clonedObj.top === undefined) return; + // 将克隆的画布重新赋值 + clonedObj.canvas = canvas; + // 设置位置信息 + clonedObj.set({ + left: clonedObj.left + grid, + top: clonedObj.top + grid, + evented: true, + id: uuid() + }); + clonedObj.forEachObject((obj: fabric.Object) => { + obj.id = uuid(); + canvas.add(obj); + }); + // 解决不可选择问题 + clonedObj.setCoords(); + canvas.setActiveObject(clonedObj); + canvas.requestRenderAll(); + }); + }); + } + + // 单个对象复制 + _copyObject(activeObject: fabric.Object) { + // 间距设置 + const grid = 10; + const canvas = this.canvas; + activeObject?.clone((cloned: fabric.Object) => { + if (cloned.left === undefined || cloned.top === undefined) return; + canvas.discardActiveObject(); + // 设置位置信息 + cloned.set({ + left: cloned.left + grid, + top: cloned.top + grid, + evented: true, + id: uuid() + }); + canvas.add(cloned); + canvas.setActiveObject(cloned); + canvas.requestRenderAll(); + }); + } + + // 复制元素 + clone(paramsActiveObeject: fabric.ActiveSelection | fabric.Object) { + const activeObject = paramsActiveObeject || this.canvas.getActiveObject(); + if (!activeObject) return; + if (activeObject?.type === "activeSelection") { + this._copyActiveSelection(activeObject); + } else { + this._copyObject(activeObject); + } + } + + // 快捷键扩展回调 + hotkeyEvent(eventName: string, e: any) { + if (eventName === "ctrl+c" && e.type === "keydown") { + const activeObject = this.canvas.getActiveObject(); + this.cache = activeObject; + } + if (eventName === "ctrl+v" && e.type === "keydown") { + if (this.cache) { + this.clone(this.cache); + } + } + } + + contextMenu() { + const activeObject = this.canvas.getActiveObject(); + if (activeObject) { + return [ + { + text: "复制", + hotkey: "Ctrl+V", + disabled: false, + onclick: () => this.clone() + } + ]; + } + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default CopyPlugin; diff --git a/src/core/plugin/DeleteHotKeyPlugin.ts b/src/core/plugin/DeleteHotKeyPlugin.ts new file mode 100644 index 0000000..c839510 --- /dev/null +++ b/src/core/plugin/DeleteHotKeyPlugin.ts @@ -0,0 +1,62 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-20 12:57:35 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:14:38 + * @Description: 删除快捷键 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; +// import { v4 as uuid } from 'uuid'; + +class DeleteHotKeyPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "DeleteHotKeyPlugin"; + static apis = ["del"]; + public hotkeys: string[] = ["backspace"]; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + // 快捷键扩展回调 + hotkeyEvent(eventName: string, e: any) { + if (e.type === "keydown" && eventName === "backspace") { + this.del(); + } + } + + del() { + const { canvas } = this; + const activeObject = canvas.getActiveObjects(); + if (activeObject) { + activeObject.map(item => canvas.remove(item)); + canvas.requestRenderAll(); + canvas.discardActiveObject(); + } + } + + contextMenu() { + const activeObject = this.canvas.getActiveObject(); + if (activeObject) { + return [ + null, + { + text: "删除", + hotkey: "Ctrl+V", + disabled: false, + onclick: () => this.del() + } + ]; + } + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default DeleteHotKeyPlugin; diff --git a/src/core/plugin/DownFontPlugin.ts b/src/core/plugin/DownFontPlugin.ts new file mode 100644 index 0000000..3d4c24a --- /dev/null +++ b/src/core/plugin/DownFontPlugin.ts @@ -0,0 +1,33 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-27 22:58:57 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:14:43 + * @Description: 下载字体插件 + */ + +import { downFontByJSON } from "@/utils/utils"; +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; + +class DownFontPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "DownFontPlugin"; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + hookImportBefore(json) { + // console.log(downFontByJSON(json).then, 111); + return downFontByJSON(json); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default DownFontPlugin; diff --git a/src/core/plugin/DrawLinePlugin.ts b/src/core/plugin/DrawLinePlugin.ts new file mode 100644 index 0000000..d26d075 --- /dev/null +++ b/src/core/plugin/DrawLinePlugin.ts @@ -0,0 +1,138 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-21 22:09:36 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:14:50 + * @Description: file content + */ + +import { v4 as uuid } from "uuid"; +import { fabric } from "fabric"; +import Arrow from "@/core/objects/Arrow"; +import Editor from "../core"; +type IEditor = Editor; + +class DrawLinePlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "DrawLinePlugin"; + static apis = ["setArrow", "setMode"]; + isDrawingLineMode: boolean; + isArrow: boolean; + lineToDraw: any; + pointer: any; + pointerPoints: any; + isDrawingLine: boolean; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + + this.isDrawingLine = false; + this.isDrawingLineMode = false; + this.isArrow = false; + this.lineToDraw = null; + this.pointer = null; + this.pointerPoints = null; + this.init(); + } + + init() { + const { canvas } = this; + canvas.on("mouse:down", o => { + if (!this.isDrawingLineMode) return; + canvas.discardActiveObject(); + canvas.getObjects().forEach(obj => { + obj.selectable = false; + obj.hasControls = false; + }); + canvas.requestRenderAll(); + this.isDrawingLine = true; + this.pointer = canvas.getPointer(o.e); + this.pointerPoints = [ + this.pointer.x, + this.pointer.y, + this.pointer.x, + this.pointer.y + ]; + + const NodeHandler = this.isArrow ? Arrow : fabric.Line; + this.lineToDraw = new NodeHandler(this.pointerPoints, { + strokeWidth: 2, + stroke: "#000000", + id: uuid() + }); + + this.lineToDraw.selectable = false; + this.lineToDraw.evented = false; + this.lineToDraw.strokeUniform = true; + canvas.add(this.lineToDraw); + }); + + canvas.on("mouse:move", o => { + if (!this.isDrawingLine) return; + canvas.discardActiveObject(); + const activeObject = canvas.getActiveObject(); + if (activeObject) return; + this.pointer = canvas.getPointer(o.e); + + if (o.e.shiftKey) { + // calc angle + const startX = this.pointerPoints[0]; + const startY = this.pointerPoints[1]; + const x2 = this.pointer.x - startX; + const y2 = this.pointer.y - startY; + const r = Math.sqrt(x2 * x2 + y2 * y2); + let angle = (Math.atan2(y2, x2) / Math.PI) * 180; + angle = parseInt(((angle + 7.5) % 360) / 15) * 15; + + const cosx = r * Math.cos((angle * Math.PI) / 180); + const sinx = r * Math.sin((angle * Math.PI) / 180); + + this.lineToDraw.set({ + x2: cosx + startX, + y2: sinx + startY + }); + } else { + this.lineToDraw.set({ + x2: this.pointer.x, + y2: this.pointer.y + }); + } + + canvas.renderAll(); + }); + + canvas.on("mouse:up", () => { + if (!this.isDrawingLine) return; + this.lineToDraw.setCoords(); + this.isDrawingLine = false; + canvas.discardActiveObject(); + }); + } + + setArrow(params: any) { + this.isArrow = params; + } + + setMode(params: any) { + this.isDrawingLineMode = params; + if (!this.isDrawingLineMode) { + this.endRest(); + } + } + + endRest() { + this.canvas.getObjects().forEach(obj => { + if (obj.id !== "workspace") { + obj.selectable = true; + obj.hasControls = true; + } + }); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default DrawLinePlugin; diff --git a/src/core/plugin/DringPlugin.ts b/src/core/plugin/DringPlugin.ts new file mode 100644 index 0000000..19eb819 --- /dev/null +++ b/src/core/plugin/DringPlugin.ts @@ -0,0 +1,126 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-05-19 08:31:34 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:14:55 + * @Description: 拖拽插件 + */ + +import Editor from "../core"; +type IEditor = Editor; + +declare type ExtCanvas = fabric.Canvas & { + isDragging: boolean; + lastPosX: number; + lastPosY: number; +}; + +class DringPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + public defautOption = {}; + static pluginName = "DringPlugin"; + static events = ["startDring", "endDring"]; + static apis = ["startDring", "endDring"]; + public hotkeys: string[] = ["space"]; + dragMode = false; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.dragMode = false; + this.init(); + } + init() { + this._initDring(); + } + + startDring() { + this.dragMode = true; + this.canvas.defaultCursor = "grab"; + this.editor.emit("startDring"); + this.canvas.renderAll(); + } + endDring() { + this.dragMode = false; + this.canvas.defaultCursor = "default"; + this.canvas.isDragging = false; + this.editor.emit("endDring"); + this.canvas.renderAll(); + } + + // 拖拽模式; + _initDring() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + this.canvas.on("mouse:down", function (this: ExtCanvas, opt) { + const evt = opt.e; + if (evt.altKey || self.dragMode) { + self.canvas.defaultCursor = "grabbing"; + self.canvas.discardActiveObject(); + self._setDring(); + this.selection = false; + this.isDragging = true; + this.lastPosX = evt.clientX; + this.lastPosY = evt.clientY; + this.requestRenderAll(); + } + }); + + this.canvas.on("mouse:move", function (this: ExtCanvas, opt) { + if (this.isDragging) { + self.canvas.discardActiveObject(); + self.canvas.defaultCursor = "grabbing"; + const { e } = opt; + if (!this.viewportTransform) return; + const vpt = this.viewportTransform; + vpt[4] += e.clientX - this.lastPosX; + vpt[5] += e.clientY - this.lastPosY; + this.lastPosX = e.clientX; + this.lastPosY = e.clientY; + this.requestRenderAll(); + } + }); + + this.canvas.on("mouse:up", function (this: ExtCanvas) { + if (!this.viewportTransform) return; + this.setViewportTransform(this.viewportTransform); + this.isDragging = false; + this.selection = true; + this.getObjects().forEach(obj => { + if (obj.id !== "workspace" && obj.hasControls) { + obj.selectable = true; + } + }); + self.canvas.defaultCursor = "default"; + this.requestRenderAll(); + }); + } + + _setDring() { + this.canvas.selection = false; + this.canvas.defaultCursor = "grab"; + this.canvas.getObjects().forEach(obj => { + obj.selectable = false; + }); + this.canvas.requestRenderAll(); + } + + destroy() { + console.log("pluginDestroy"); + } + + // 快捷键扩展回调 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hotkeyEvent(eventName: string, e: any) { + if (e.code === "Space" && e.type === "keydown") { + if (!this.dragMode) { + this.startDring(); + } + } + if (e.code === "Space" && e.type === "keyup") { + this.endDring(); + } + } +} + +export default DringPlugin; diff --git a/src/core/plugin/FlipPlugin.ts b/src/core/plugin/FlipPlugin.ts new file mode 100644 index 0000000..651a295 --- /dev/null +++ b/src/core/plugin/FlipPlugin.ts @@ -0,0 +1,76 @@ +/* + * @Author: donghao donghao@supervision.ltd + * @Date: 2024-08-06 17:11:20 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:29:22 + * @FilePath: \General-AI-Platform-Web-Client\src\core\plugin\FlipPlugin.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +import { fabric } from "fabric"; +import type Editor from "../core"; +import { SelectEvent, SelectMode } from "@/utils/event/types"; +import { Ref } from "vue"; +import { $t } from "@/plugins/i18n"; +import event from "@/utils/event/notifier"; + +export default class FlipPlugin { + public canvas: fabric.Canvas; + public editor: Editor; + static pluginName = "FlipPlugin"; + static apis = ["flip"]; + selectedMode: Ref; + constructor(canvas: fabric.Canvas, editor: Editor) { + this.canvas = canvas; + this.editor = editor; + this.selectedMode = ref(SelectMode.EMPTY); + + this.init(); + } + + init() { + event.on(SelectEvent.ONE, () => (this.selectedMode.value = SelectMode.ONE)); + event.on( + SelectEvent.MULTI, + () => (this.selectedMode.value = SelectMode.MULTI) + ); + event.on( + SelectEvent.CANCEL, + () => (this.selectedMode.value = SelectMode.EMPTY) + ); + } + + flip(type: "X" | "Y") { + const activeObject = this.canvas.getActiveObject(); + if (activeObject) { + activeObject.set(`flip${type}`, !activeObject[`flip${type}`]).setCoords(); + this.canvas.requestRenderAll(); + } + } + + contextMenu() { + if (this.selectedMode.value === SelectMode.ONE) { + return [ + { + text: "翻转", + hotkey: "❯", + subitems: [ + { + text: $t("flip.x"), + hotkey: "|", + onclick: () => this.flip("X") + }, + { + text: $t("flip.y"), + hotkey: "-", + onclick: () => this.flip("Y") + } + ] + } + ]; + } + } + + destroy() { + console.log("pluginDestroy"); + } +} diff --git a/src/core/plugin/GroupAlignPlugin.ts b/src/core/plugin/GroupAlignPlugin.ts new file mode 100644 index 0000000..5f9da19 --- /dev/null +++ b/src/core/plugin/GroupAlignPlugin.ts @@ -0,0 +1,338 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-22 16:19:46 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:29:36 + * @Description: 组对齐插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; + +class GroupAlignPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "GroupAlignPlugin"; + static apis = [ + "left", + "right", + "xcenter", + "ycenter", + "top", + "bottom", + "xequation", + "yequation" + ]; + // public hotkeys: string[] = ['space']; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + left() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // const activeObjectLeft = -(activeObject.width / 2); + // activeSelection.forEachObject((item) => { + // item.set({ + // left: activeObjectLeft, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { left } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + left: left - bounding.left + item.left + }); + item.setCoords(); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + right() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // const activeObjectLeft = activeObject.width / 2; + // activeSelection.forEachObject((item) => { + // item.set({ + // left: activeObjectLeft - item.width * item.scaleX, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { left, width } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + left: left + width - (bounding.left + bounding.width) + item.left + }); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + xcenter() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // activeSelection.forEachObject((item) => { + // item.set({ + // left: 0 - (item.width * item.scaleX) / 2, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { left, width } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + left: + left + width / 2 - (bounding.left + bounding.width / 2) + item.left + }); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + ycenter() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // activeSelection.forEachObject((item) => { + // item.set({ + // top: 0 - (item.height * item.scaleY) / 2, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { top, height } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + top: top + height / 2 - (bounding.top + bounding.height / 2) + item.top + }); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + top() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // const activeObjectTop = -(activeObject.height / 2); + // activeSelection.forEachObject((item) => { + // item.set({ + // top: activeObjectTop, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { top } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + top: top - bounding.top + item.top + }); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + bottom() { + const { canvas } = this; + // const activeObject = canvas.getActiveObject(); + // if (activeObject && activeObject.type === 'activeSelection') { + // const activeSelection = activeObject; + // const activeObjectTop = activeObject.height / 2; + // activeSelection.forEachObject((item) => { + // item.set({ + // top: activeObjectTop - item.height * item.scaleY, + // }); + // item.setCoords(); + // canvas.renderAll(); + // }); + // } + + const activeObject = canvas.getActiveObject(); + const selectObjects = canvas.getActiveObjects(); + const { top, height } = activeObject; + canvas.discardActiveObject(); + selectObjects.forEach(item => { + const bounding = item.getBoundingRect(true); + item.set({ + top: top + height - (bounding.top + bounding.height) + item.top + }); + }); + const activeSelection = new fabric.ActiveSelection(selectObjects, { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + canvas.requestRenderAll(); + } + + xequation() { + const { canvas } = this; + const activeObject = canvas.getActiveObject(); + + // width属性不准确,需要坐标换算 + function getItemWidth(item) { + return item.aCoords.tr.x - item.aCoords.tl.x; + } + + // 获取所有元素高度 + function getAllItemHeight() { + let count = 0; + activeObject.forEachObject(item => { + count += getItemWidth(item); + }); + return count; + } + // 获取平均间距 + function spacWidth() { + const count = getAllItemHeight(); + const allSpac = activeObject.width - count; + return allSpac / (activeObject._objects.length - 1); + } + + // 获取当前元素之前所有元素的高度 + function getItemLeft(i) { + if (i === 0) return 0; + let width = 0; + for (let index = 0; index < i; index++) { + width += getItemWidth(activeObject._objects[index]); + } + return width; + } + + if (activeObject && activeObject.type === "activeSelection") { + const activeSelection = activeObject; + // 排序 + activeSelection._objects.sort((a, b) => a.left - b.left); + + // 平均间距计算 + const itemSpac = spacWidth(); + // 组原点高度 + const yHeight = activeObject.width / 2; + + activeObject.forEachObject((item, i) => { + // 获取当前元素之前所有元素的高度 + const preHeight = getItemLeft(i); + // 顶部距离 间距 * 索引 + 之前元素高度 - 原点高度 + const top = itemSpac * i + preHeight - yHeight; + item.set("left", top); + }); + canvas.renderAll(); + } + } + + yequation() { + const { canvas } = this; + const activeObject = canvas.getActiveObject(); + // width属性不准确,需要坐标换算 + function getItemHeight(item) { + return item.aCoords.bl.y - item.aCoords.tl.y; + } + // 获取所有元素高度 + function getAllItemHeight() { + let count = 0; + activeObject.forEachObject(item => { + count += getItemHeight(item); + }); + return count; + } + // 获取平均间距 + function spacHeight() { + const count = getAllItemHeight(); + const allSpac = activeObject.height - count; + return allSpac / (activeObject._objects.length - 1); + } + + // 获取当前元素之前所有元素的高度 + function getItemTop(i) { + if (i === 0) return 0; + let height = 0; + for (let index = 0; index < i; index++) { + height += getItemHeight(activeObject._objects[index]); + } + return height; + } + + if (activeObject && activeObject.type === "activeSelection") { + const activeSelection = activeObject; + // 排序 + activeSelection._objects.sort((a, b) => a.top - b.top); + + // 平均间距计算 + const itemSpac = spacHeight(); + // 组原点高度 + const yHeight = activeObject.height / 2; + + activeObject.forEachObject((item, i) => { + // 获取当前元素之前所有元素的高度 + const preHeight = getItemTop(i); + // 顶部距离 间距 * 索引 + 之前元素高度 - 原点高度 + const top = itemSpac * i + preHeight - yHeight; + item.set("top", top); + }); + canvas.renderAll(); + } + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default GroupAlignPlugin; diff --git a/src/core/plugin/GroupPlugin.ts b/src/core/plugin/GroupPlugin.ts new file mode 100644 index 0000000..2fcba23 --- /dev/null +++ b/src/core/plugin/GroupPlugin.ts @@ -0,0 +1,83 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-20 13:21:10 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:29:41 + * @Description: 组合拆分组合插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +import { v4 as uuid } from "uuid"; +type IEditor = Editor; + +class GroupPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "GroupPlugin"; + static apis = ["unGroup", "group"]; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + // 拆分组 + unGroup() { + const activeObject = this.canvas.getActiveObject() as fabric.Group; + if (!activeObject) return; + // 先获取当前选中的对象,然后打散 + activeObject.toActiveSelection(); + activeObject.getObjects().forEach((item: fabric.Object) => { + item.set("id", uuid()); + }); + this.canvas.discardActiveObject().renderAll(); + } + + group() { + // 组合元素 + const activeObj = this.canvas.getActiveObject() as fabric.ActiveSelection; + if (!activeObj) return; + const activegroup = activeObj.toGroup(); + const objectsInGroup = activegroup.getObjects(); + activegroup.clone((newgroup: fabric.Group) => { + newgroup.set("id", uuid()); + this.canvas.remove(activegroup); + objectsInGroup.forEach(object => { + this.canvas.remove(object); + }); + this.canvas.add(newgroup); + this.canvas.setActiveObject(newgroup); + }); + } + + contextMenu() { + const activeObject = this.canvas.getActiveObject(); + console.log(activeObject, "111"); + if (activeObject && activeObject.type === "group") { + return [ + { + text: "拆分组合", + hotkey: "Ctrl+V", + disabled: false, + onclick: () => this.unGroup() + } + ]; + } + + if (this.canvas.getActiveObjects().length > 1) { + return [ + { + text: "组合", + hotkey: "Ctrl+V", + disabled: false, + onclick: () => this.group() + } + ]; + } + } + destroy() { + console.log("pluginDestroy"); + } +} + +export default GroupPlugin; diff --git a/src/core/plugin/GroupTextEditorPlugin.ts b/src/core/plugin/GroupTextEditorPlugin.ts new file mode 100644 index 0000000..aa38438 --- /dev/null +++ b/src/core/plugin/GroupTextEditorPlugin.ts @@ -0,0 +1,202 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-22 16:11:40 + * @LastEditors: 秦少卫 + * @LastEditTime: 2023-08-07 23:24:36 + * @Description: 组内文字编辑 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +import { v4 as uuid } from "uuid"; +type IEditor = Editor; + +class GroupTextEditorPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "GroupTextEditorPlugin"; + isDown = false; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this._init(); + } + + // 组内文本输入 + _init() { + this.canvas.on("mouse:down", opt => { + this.isDown = true; + // 重置选中controls + if ( + opt.target && + !opt.target.lockMovementX && + !opt.target.lockMovementY && + !opt.target.lockRotation && + !opt.target.lockScalingX && + !opt.target.lockScalingY + ) { + opt.target.hasControls = true; + } + }); + + this.canvas.on("mouse:up", () => { + this.isDown = false; + }); + + this.canvas.on("mouse:dblclick", opt => { + if (opt.target && opt.target.type === "group") { + const selectedObject = this._getGroupObj(opt) as fabric.IText; + if (!selectedObject) return; + selectedObject.selectable = true; + // 由于组内的元素,双击以后会导致controls偏移,因此隐藏他 + if (selectedObject.hasControls) { + selectedObject.hasControls = false; + } + if (this.isText(selectedObject)) { + this._bedingTextEditingEvent(selectedObject, opt); + return; + } + this.canvas.setActiveObject(selectedObject); + this.canvas.renderAll(); + } + }); + } + + // 获取点击区域内的组内文字元素 + _getGroupTextObj(opt: fabric.IEvent) { + const pointer = this.canvas.getPointer(opt.e, true); + const clickObj = this.canvas._searchPossibleTargets( + opt.target?._objects, + pointer + ); + if (clickObj && this.isText(clickObj)) { + return clickObj; + } + return false; + } + + _getGroupObj(opt: fabric.IEvent) { + const pointer = this.canvas.getPointer(opt.e, true); + const clickObj = this.canvas._searchPossibleTargets( + opt.target?._objects, + pointer + ); + return clickObj; + } + + // 通过组合重新组装来编辑文字,可能会耗性能。 + _bedingTextEditingEvent( + textObject: fabric.IText, + opt: fabric.IEvent + ) { + if (!opt.target) return; + const textObjectJSON = textObject.toObject(); + const groupObj = opt.target; + + const ftype: any = { + "i-text": "IText", + text: "Text", + textbox: "Textbox" + }; + + const eltype: string = ftype[textObjectJSON.type]; + + const groupMatrix: number[] = groupObj.calcTransformMatrix(); + + const a: number = groupMatrix[0]; + const b: number = groupMatrix[1]; + const c: number = groupMatrix[2]; + const d: number = groupMatrix[3]; + const e: number = groupMatrix[4]; + const f: number = groupMatrix[5]; + + const newX = a * textObject.left + c * textObject.top + e; + const newY = b * textObject.left + d * textObject.top + f; + + const tempText = new fabric[eltype](textObject.text, { + ...textObjectJSON, + textAlign: textObject.textAlign, + left: newX, + top: newY, + styles: textObject.styles, + groupCopyed: textObject.group + }); + tempText.id = uuid(); + textObject.visible = false; + opt.target.addWithUpdate(); + tempText.visible = true; + tempText.selectable = true; + tempText.hasConstrols = false; + tempText.editable = true; + this.canvas.add(tempText); + this.canvas.setActiveObject(tempText); + tempText.enterEditing(); + tempText.selectAll(); + + tempText.on("editing:exited", () => { + // 进入编辑模式时触发 + textObject.set({ + text: tempText.text, + visible: true + }); + opt.target.addWithUpdate(); + tempText.visible = false; + this.canvas.remove(tempText); + this.canvas.setActiveObject(opt.target); + }); + } + + // 绑定编辑取消事件 + _bedingEditingEvent( + textObject: fabric.IText, + opt: fabric.IEvent + ) { + if (!opt.target) return; + const left = opt.target.left; + const top = opt.target.top; + const ids = this._unGroup() || []; + + const resetGroup = () => { + const groupArr = this.canvas + .getObjects() + .filter(item => item.id && ids.includes(item.id)); + // 删除元素 + groupArr.forEach(item => this.canvas.remove(item)); + + // 生成新组 + const group = new fabric.Group([...groupArr]); + group.set("left", left); + group.set("top", top); + group.set("id", uuid()); + textObject.off("editing:exited", resetGroup); + this.canvas.add(group); + this.canvas.discardActiveObject().renderAll(); + }; + // 绑定取消事件 + textObject.on("editing:exited", resetGroup); + } + + // 拆分组合并返回ID + _unGroup() { + const ids: string[] = []; + const activeObj = this.canvas.getActiveObject() as fabric.Group; + if (!activeObj) return; + activeObj.getObjects().forEach(item => { + const id = uuid(); + ids.push(id); + item.set("id", id); + }); + activeObj.toActiveSelection(); + return ids; + } + + isText(obj: fabric.Object) { + return obj.type && ["i-text", "text", "textbox"].includes(obj.type); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default GroupTextEditorPlugin; diff --git a/src/core/plugin/HistoryPlugin.ts b/src/core/plugin/HistoryPlugin.ts new file mode 100644 index 0000000..e1461ff --- /dev/null +++ b/src/core/plugin/HistoryPlugin.ts @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * @Author: 秦少卫 + * @Date: 2023-06-20 13:06:31 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:29:49 + * @Description: 历史记录插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +import { ref } from "vue"; +import { useRefHistory } from "@vueuse/core"; +type IEditor = Editor; +// import { v4 as uuid } from 'uuid'; + +class HistoryPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "HistoryPlugin"; + static apis = ["undo", "redo", "getHistory"]; + static events = ["historyInitSuccess"]; + public hotkeys: string[] = ["ctrl+z"]; + history: any; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + + this._init(); + } + + _init() { + this.history = useRefHistory(ref(), { + capacity: 50 + }); + this.canvas.on({ + "object:added": event => this._save(event), + "object:modified": event => this._save(event), + "selection:updated": event => this._save(event) + }); + } + + getHistory() { + return this.history; + } + _save(event) { + // 过滤选择元素事件 + const isSelect = event.action === undefined && event.e; + if (isSelect || !this.canvas) return; + const workspace = this.canvas + .getObjects() + .find(item => item.id === "workspace"); + if (!workspace) { + return; + } + if (this.history.isTracking.value) { + this.history.source.value = this.editor.getJson(); + } + } + + undo() { + if (this.history.canUndo.value) { + this.renderCanvas(); + this.history.undo(); + } + } + + redo() { + this.history.redo(); + this.renderCanvas(); + } + + renderCanvas = () => { + this.history.pause(); + this.canvas.clear(); + this.canvas.loadFromJSON(this.history.source.value, () => { + this.canvas.renderAll(); + this.history.resume(); + }); + }; + + // 快捷键扩展回调 + hotkeyEvent(eventName: string, e: any) { + if (eventName === "ctrl+z" && e.type === "keydown") { + this.undo(); + } + } + destroy() { + console.log("pluginDestroy"); + } +} + +export default HistoryPlugin; diff --git a/src/core/plugin/LayerPlugin.ts b/src/core/plugin/LayerPlugin.ts new file mode 100644 index 0000000..2a761ff --- /dev/null +++ b/src/core/plugin/LayerPlugin.ts @@ -0,0 +1,113 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-15 23:23:18 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:29:53 + * @Description: 图层调整插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; + +class LayerPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "LayerPlugin"; + static apis = ["up", "upTop", "down", "downTop"]; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + _getWorkspace() { + return this.canvas.getObjects().find(item => item.id === "workspace"); + } + + _workspaceSendToBack() { + const workspace = this._getWorkspace(); + workspace && workspace.sendToBack(); + } + + up() { + const actives = this.canvas.getActiveObjects(); + if (actives && actives.length === 1) { + const activeObject = this.canvas.getActiveObjects()[0]; + activeObject && activeObject.bringForward(); + this.canvas.renderAll(); + this._workspaceSendToBack(); + } + } + + upTop() { + const actives = this.canvas.getActiveObjects(); + if (actives && actives.length === 1) { + const activeObject = this.canvas.getActiveObjects()[0]; + activeObject && activeObject.bringToFront(); + this.canvas.renderAll(); + console.log(this); + this._workspaceSendToBack(); + } + } + + down() { + const actives = this.canvas.getActiveObjects(); + if (actives && actives.length === 1) { + const activeObject = this.canvas.getActiveObjects()[0]; + activeObject && activeObject.sendBackwards(); + this.canvas.renderAll(); + this._workspaceSendToBack(); + } + } + + downTop() { + const actives = this.canvas.getActiveObjects(); + if (actives && actives.length === 1) { + const activeObject = this.canvas.getActiveObjects()[0]; + activeObject && activeObject.sendToBack(); + this.canvas.renderAll(); + this._workspaceSendToBack(); + } + } + + contextMenu() { + const activeObject = this.canvas.getActiveObject(); + if (activeObject) { + return [ + { + text: "图层管理", + hotkey: "❯", + subitems: [ + { + text: "上一个", + hotkey: "key", + onclick: () => this.up() + }, + { + text: "下一个", + hotkey: "key", + onclick: () => this.down() + }, + { + text: "置顶", + hotkey: "key", + onclick: () => this.upTop() + }, + { + text: "置底", + hotkey: "key", + onclick: () => this.downTop() + } + ] + } + ]; + // return [{ text: '复制', hotkey: 'Ctrl+V', disabled: false, onclick: () => this.clone() }]; + } + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default LayerPlugin; diff --git a/src/core/plugin/MaterialPlugin.ts b/src/core/plugin/MaterialPlugin.ts new file mode 100644 index 0000000..900c45d --- /dev/null +++ b/src/core/plugin/MaterialPlugin.ts @@ -0,0 +1,45 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-08-04 21:13:16 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:30:01 + * @Description: 素材插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; +import axios from "axios"; + +class MaterialPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "MaterialPlugin"; + static apis = ["getMaterialType", "getMaterialList"]; + apiMapUrl: { [propName: string]: string }; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + + this.apiMapUrl = { + template: + "https://nihaojob.github.io/vue-fabric-editor-static/template/type.json", + svg: "https://nihaojob.github.io/vue-fabric-editor-static/svg/type.json" + }; + } + + // 根据素材类型获取分裂列表 + async getMaterialType(typeId: string) { + const url = this.apiMapUrl[typeId]; + const res = await axios.get(url, { params: { typeId } }); + return res.data.data; + } + + async getMaterialInfo(typeId: string) { + const url = this.apiMapUrl[typeId]; + const res = await axios.get(url, { params: { typeId } }); + return res.data.data; + } +} + +export default MaterialPlugin; diff --git a/src/core/plugin/MoveHotKeyPlugin.ts b/src/core/plugin/MoveHotKeyPlugin.ts new file mode 100644 index 0000000..09776d9 --- /dev/null +++ b/src/core/plugin/MoveHotKeyPlugin.ts @@ -0,0 +1,58 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-20 12:52:09 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:30:04 + * @Description: 移动快捷键 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +type IEditor = Editor; +// import { v4 as uuid } from 'uuid'; + +class MoveHotKeyPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "MoveHotKeyPlugin"; + public hotkeys: string[] = ["left", "right", "down", "up"]; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + } + + // 快捷键扩展回调 + hotkeyEvent(eventName: string, e: any) { + if (e.type === "keydown") { + const { canvas } = this; + const activeObject = canvas.getActiveObject(); + if (!activeObject) return; + switch (eventName) { + case "left": + if (activeObject.left === undefined) return; + activeObject.set("left", activeObject.left - 1); + break; + case "right": + if (activeObject.left === undefined) return; + activeObject.set("left", activeObject.left + 1); + break; + case "down": + if (activeObject.top === undefined) return; + activeObject.set("top", activeObject.top + 1); + break; + case "up": + if (activeObject.top === undefined) return; + activeObject.set("top", activeObject.top - 1); + break; + default: + } + canvas.renderAll(); + } + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default MoveHotKeyPlugin; diff --git a/src/core/plugin/RulerPlugin.ts b/src/core/plugin/RulerPlugin.ts new file mode 100644 index 0000000..31062d6 --- /dev/null +++ b/src/core/plugin/RulerPlugin.ts @@ -0,0 +1,73 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-07-04 23:45:49 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:30:08 + * @Description: 标尺插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +// import { throttle } from 'lodash-es'; +type IEditor = Editor; + +import initRuler from "@/core/ruler"; + +class RulerPlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "RulerPlugin"; + // static events = ['sizeChange']; + static apis = [ + "hideGuideline", + "showGuideline", + "rulerEnable", + "rulerDisable" + ]; + ruler: any; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.init(); + } + + hookSaveBefore() { + return new Promise(resolve => { + this.hideGuideline(); + resolve(true); + }); + } + + hookSaveAfter() { + return new Promise(resolve => { + this.showGuideline(); + resolve(true); + }); + } + + init() { + this.ruler = initRuler(this.canvas); + } + + hideGuideline() { + this.ruler.hideGuideline(); + } + + showGuideline() { + this.ruler.showGuideline(); + } + + rulerEnable() { + this.ruler.enable(); + } + + rulerDisable() { + this.ruler.disable(); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default RulerPlugin; diff --git a/src/core/plugin/WorkspacePlugin.ts b/src/core/plugin/WorkspacePlugin.ts new file mode 100644 index 0000000..c1182b5 --- /dev/null +++ b/src/core/plugin/WorkspacePlugin.ts @@ -0,0 +1,232 @@ +/* + * @Author: 秦少卫 + * @Date: 2023-06-27 12:26:41 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:30:13 + * @Description: 画布区域插件 + */ + +import { fabric } from "fabric"; +import Editor from "../core"; +import { throttle } from "lodash-es"; +type IEditor = Editor; + +class WorkspacePlugin { + public canvas: fabric.Canvas; + public editor: IEditor; + static pluginName = "WorkspacePlugin"; + static events = ["sizeChange"]; + static apis = ["big", "small", "auto", "one", "setSize"]; + workspaceEl: HTMLElement; + workspace: null | fabric.Rect; + option: any; + constructor(canvas: fabric.Canvas, editor: IEditor) { + this.canvas = canvas; + this.editor = editor; + this.init({ + width: 900, + height: 2000 + }); + } + + init(option) { + const workspaceEl = document.querySelector("#workspace") as HTMLElement; + if (!workspaceEl) { + throw new Error("element #workspace is missing, plz check!"); + } + this.workspaceEl = workspaceEl; + this.workspace = null; + this.option = option; + this._initBackground(); + this._initWorkspace(); + this._initResizeObserve(); + this._bindWheel(); + } + + // hookImportBefore() { + // return new Promise((resolve, reject) => { + // resolve(); + // }); + // } + + hookImportAfter() { + return new Promise(resolve => { + const workspace = this.canvas + .getObjects() + .find(item => item.id === "workspace"); + if (workspace) { + workspace.set("selectable", false); + workspace.set("hasControls", false); + this.setSize(workspace.width, workspace.height); + this.editor.emit("sizeChange", workspace.width, workspace.height); + } + resolve(); + }); + } + + hookSaveAfter() { + return new Promise(resolve => { + this.auto(); + resolve(true); + }); + } + + // 初始化背景 + _initBackground() { + this.canvas.backgroundImage = ""; + this.canvas.setWidth(this.workspaceEl.offsetWidth); + this.canvas.setHeight(this.workspaceEl.offsetHeight); + } + + // 初始化画布 + _initWorkspace() { + const { width, height } = this.option; + const workspace = new fabric.Rect({ + fill: "rgba(255,255,255,1)", + width, + height, + id: "workspace", + strokeWidth: 0 + }); + workspace.set("selectable", false); + workspace.set("hasControls", false); + workspace.hoverCursor = "default"; + this.canvas.add(workspace); + this.canvas.renderAll(); + + this.workspace = workspace; + this.auto(); + } + + /** + * 设置画布中心到指定对象中心点上 + * @param {Object} obj 指定的对象 + */ + setCenterFromObject(obj: fabric.Rect) { + const { canvas } = this; + const objCenter = obj.getCenterPoint(); + const viewportTransform = canvas.viewportTransform; + if ( + canvas.width === undefined || + canvas.height === undefined || + !viewportTransform + ) + return; + viewportTransform[4] = + canvas.width / 2 - objCenter.x * viewportTransform[0]; + viewportTransform[5] = + canvas.height / 2 - objCenter.y * viewportTransform[3]; + canvas.setViewportTransform(viewportTransform); + canvas.renderAll(); + } + + // 初始化监听器 + _initResizeObserve() { + const resizeObserver = new ResizeObserver( + throttle(() => { + this.auto(); + }, 50) + ); + resizeObserver.observe(this.workspaceEl); + } + + setSize(width: number, height: number) { + this._initBackground(); + this.option.width = width; + this.option.height = height; + // 重新设置workspace + this.workspace = this.canvas + .getObjects() + .find(item => item.id === "workspace") as fabric.Rect; + this.workspace.set("width", width); + this.workspace.set("height", height); + this.auto(); + } + + setZoomAuto(scale: number, cb?: (left?: number, top?: number) => void) { + const { workspaceEl } = this; + const width = workspaceEl.offsetWidth; + const height = workspaceEl.offsetHeight; + this.canvas.setWidth(width); + this.canvas.setHeight(height); + const center = this.canvas.getCenter(); + this.canvas.setViewportTransform(fabric.iMatrix.concat()); + this.canvas.zoomToPoint(new fabric.Point(center.left, center.top), scale); + if (!this.workspace) return; + this.setCenterFromObject(this.workspace); + + // 超出画布不展示 + this.workspace.clone((cloned: fabric.Rect) => { + this.canvas.clipPath = cloned; + this.canvas.requestRenderAll(); + }); + if (cb) cb(this.workspace.left, this.workspace.top); + } + + _getScale() { + const viewPortWidth = this.workspaceEl.offsetWidth; + const viewPortHeight = this.workspaceEl.offsetHeight; + // 按照宽度 + if ( + viewPortWidth / viewPortHeight < + this.option.width / this.option.height + ) { + return viewPortWidth / this.option.width; + } // 按照宽度缩放 + return viewPortHeight / this.option.height; + } + + // 放大 + big() { + let zoomRatio = this.canvas.getZoom(); + zoomRatio += 0.05; + const center = this.canvas.getCenter(); + this.canvas.zoomToPoint( + new fabric.Point(center.left, center.top), + zoomRatio + ); + } + + // 缩小 + small() { + let zoomRatio = this.canvas.getZoom(); + zoomRatio -= 0.05; + const center = this.canvas.getCenter(); + this.canvas.zoomToPoint( + new fabric.Point(center.left, center.top), + zoomRatio < 0 ? 0.01 : zoomRatio + ); + } + + // 自动缩放 + auto() { + const scale = this._getScale(); + this.setZoomAuto(scale - 0.08); + } + + // 1:1 放大 + one() { + this.setZoomAuto(0.8 - 0.08); + this.canvas.requestRenderAll(); + } + + _bindWheel() { + this.canvas.on("mouse:wheel", function (this: fabric.Canvas, opt) { + const delta = opt.e.deltaY; + let zoom = this.getZoom(); + zoom *= 0.999 ** delta; + if (zoom > 20) zoom = 20; + if (zoom < 0.01) zoom = 0.01; + const center = this.getCenter(); + this.zoomToPoint(new fabric.Point(center.left, center.top), zoom); + opt.e.preventDefault(); + opt.e.stopPropagation(); + }); + } + + destroy() { + console.log("pluginDestroy"); + } +} + +export default WorkspacePlugin; diff --git a/src/core/ruler/guideline.ts b/src/core/ruler/guideline.ts new file mode 100644 index 0000000..0fe457e --- /dev/null +++ b/src/core/ruler/guideline.ts @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { fabric } from "fabric"; + +export function setupGuideLine() { + if (fabric.GuideLine) { + return; + } + + fabric.GuideLine = fabric.util.createClass(fabric.Line, { + type: "GuideLine", + selectable: false, + hasControls: false, + hasBorders: false, + stroke: "#4bec13", + originX: "center", + originY: "center", + padding: 4, // 填充,让辅助线选择范围更大,方便选中 + globalCompositeOperation: "difference", + axis: "horizontal", + // excludeFromExport: true, + + initialize(points, options) { + const isHorizontal = options.axis === "horizontal"; + // 指针 + this.hoverCursor = isHorizontal ? "ns-resize" : "ew-resize"; + // 设置新的点 + const newPoints = isHorizontal + ? [-999999, points, 999999, points] + : [points, -999999, points, 999999]; + // 锁定移动 + options[isHorizontal ? "lockMovementX" : "lockMovementY"] = true; + // 调用父类初始化 + this.callSuper("initialize", newPoints, options); + + // 绑定事件 + this.on("mousedown:before", e => { + if (this.activeOn === "down") { + // 设置selectable:false后激活对象才能进行移动 + this.canvas.setActiveObject(this, e.e); + } + }); + + this.on("moving", e => { + if (this.canvas.ruler.options.enabled && this.isPointOnRuler(e.e)) { + this.moveCursor = "not-allowed"; + } else { + this.moveCursor = this.isHorizontal() ? "ns-resize" : "ew-resize"; + } + this.canvas.fire("guideline:moving", { + target: this, + e: e.e + }); + }); + + this.on("mouseup", e => { + // 移动到标尺上,移除辅助线 + if (this.canvas.ruler.options.enabled && this.isPointOnRuler(e.e)) { + // console.log('移除辅助线', this); + this.canvas.remove(this); + return; + } + this.moveCursor = this.isHorizontal() ? "ns-resize" : "ew-resize"; + this.canvas.fire("guideline:mouseup", { + target: this, + e: e.e + }); + }); + + this.on("removed", () => { + this.off("removed"); + this.off("mousedown:before"); + this.off("moving"); + this.off("mouseup"); + }); + }, + + getBoundingRect(absolute, calculate) { + this.bringToFront(); + + const isHorizontal = this.isHorizontal(); + const rect = this.callSuper("getBoundingRect", absolute, calculate); + rect[isHorizontal ? "top" : "left"] += + rect[isHorizontal ? "height" : "width"] / 2; + rect[isHorizontal ? "height" : "width"] = 0; + return rect; + }, + + isPointOnRuler(e) { + const isHorizontal = this.isHorizontal(); + const hoveredRuler = this.canvas.ruler.isPointOnRuler( + new fabric.Point(e.offsetX, e.offsetY) + ); + if ( + (isHorizontal && hoveredRuler === "horizontal") || + (!isHorizontal && hoveredRuler === "vertical") + ) { + return hoveredRuler; + } + return false; + }, + + isHorizontal() { + return this.height === 0; + } + } as fabric.IGuideLineClassOptions); + + fabric.GuideLine.fromObject = function (object, callback) { + const clone = fabric.util.object.clone as ( + object: any, + deep: boolean + ) => any; + + function _callback(instance: any) { + delete instance.xy; + callback && callback(instance); + } + + const options = clone(object, true); + const isHorizontal = options.height === 0; + + options.xy = isHorizontal ? options.y1 : options.x1; + options.axis = isHorizontal ? "horizontal" : "vertical"; + + fabric.Object._fromObject(options.type, options, _callback, "xy"); + }; +} + +export default fabric.GuideLine; diff --git a/src/core/ruler/index.ts b/src/core/ruler/index.ts new file mode 100644 index 0000000..6c32c71 --- /dev/null +++ b/src/core/ruler/index.ts @@ -0,0 +1,90 @@ +import type { Canvas } from "fabric/fabric-impl"; +import { fabric } from "fabric"; +import CanvasRuler, { RulerOptions } from "./ruler"; + +function initRuler(canvas: Canvas, options?: RulerOptions) { + const ruler = new CanvasRuler({ + canvas, + ...options + }); + + // 辅助线移动到画板外删除 + let workspace: fabric.Object | undefined = undefined; + + /** + * 获取workspace + */ + const getWorkspace = () => { + workspace = canvas.getObjects().find(item => item.id === "workspace"); + }; + + /** + * 判断target是否在object矩形外 + * @param object + * @param target + * @returns + */ + const isRectOut = ( + object: fabric.Object, + target: fabric.GuideLine + ): boolean => { + const { top, height, left, width } = object; + + if ( + top === undefined || + height === undefined || + left === undefined || + width === undefined + ) { + return false; + } + + const targetRect = target.getBoundingRect(true, true); + const { + top: targetTop, + height: targetHeight, + left: targetLeft, + width: targetWidth + } = targetRect; + + if ( + target.isHorizontal() && + (top > targetTop + 1 || top + height < targetTop + targetHeight - 1) + ) { + return true; + } else if ( + !target.isHorizontal() && + (left > targetLeft + 1 || left + width < targetLeft + targetWidth - 1) + ) { + return true; + } + + return false; + }; + + canvas.on("guideline:moving", e => { + if (!workspace) { + getWorkspace(); + return; + } + const { target } = e; + if (isRectOut(workspace, target)) { + target.moveCursor = "not-allowed"; + } + }); + + canvas.on("guideline:mouseup", e => { + if (!workspace) { + getWorkspace(); + return; + } + const { target } = e; + if (isRectOut(workspace, target)) { + canvas.remove(target); + canvas.setCursor(canvas.defaultCursor ?? ""); + } + }); + return ruler; +} + +export default initRuler; diff --git a/src/core/ruler/ruler.ts b/src/core/ruler/ruler.ts new file mode 100644 index 0000000..38e45ec --- /dev/null +++ b/src/core/ruler/ruler.ts @@ -0,0 +1,655 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Canvas, Point, IEvent } from "fabric/fabric-impl"; +import { fabric } from "fabric"; +import { + getGap, + mergeLines, + darwRect, + darwText, + darwLine, + drawMask +} from "./utils"; +import { throttle } from "lodash-es"; +import { setupGuideLine } from "./guideline"; + +/** + * 配置 + */ +export interface RulerOptions { + /** + * Canvas + */ + canvas: Canvas; + + /** + * 标尺宽高 + * @default 20 + */ + ruleSize?: number; + + /** + * 字体大小 + * @default 10 + */ + fontSize?: number; + + /** + * 是否开启标尺 + * @default false + */ + enabled?: boolean; + + /** + * 背景颜色 + */ + backgroundColor?: string; + + /** + * 文字颜色 + */ + textColor?: string; + + /** + * 边框颜色 + */ + borderColor?: string; + + /** + * 高亮颜色 + */ + highlightColor?: string; +} + +export type Rect = { left: number; top: number; width: number; height: number }; + +export type HighlightRect = { + skip?: "x" | "y"; +} & Rect; + +class CanvasRuler { + protected ctx: CanvasRenderingContext2D; + + /** + * 配置 + */ + public options: Required; + + /** + * 标尺起始点 + */ + public startCalibration: undefined | Point; + + private activeOn: "down" | "up" = "up"; + + /** + * 选取对象矩形坐标 + */ + private objectRect: + | undefined + | { + x: HighlightRect[]; + y: HighlightRect[]; + }; + + /** + * 事件句柄缓存 + */ + private eventHandler: Record void> = { + // calcCalibration: this.calcCalibration.bind(this), + calcObjectRect: throttle(this.calcObjectRect.bind(this), 15), + clearStatus: this.clearStatus.bind(this), + canvasMouseDown: this.canvasMouseDown.bind(this), + canvasMouseMove: throttle(this.canvasMouseMove.bind(this), 15), + canvasMouseUp: this.canvasMouseUp.bind(this), + render: (e: any) => { + // 避免多次渲染 + if (!e.ctx) return; + this.render(); + } + }; + + private lastAttr: { + status: "out" | "horizontal" | "vertical"; + cursor: string | undefined; + selection: boolean | undefined; + } = { + status: "out", + cursor: undefined, + selection: undefined + }; + + private tempGuidelLine: fabric.GuideLine | undefined; + + constructor(_options: RulerOptions) { + // 合并默认配置 + this.options = Object.assign( + { + ruleSize: 20, + fontSize: 10, + enabled: false, + backgroundColor: "#fff", + borderColor: "#ddd", + highlightColor: "#007fff", + textColor: "#888" + }, + _options + ); + + this.ctx = this.options.canvas.getContext(); + + fabric.util.object.extend(this.options.canvas, { + ruler: this + }); + + setupGuideLine(); + + if (this.options.enabled) { + this.enable(); + } + } + + // 销毁 + public destroy() { + this.disable(); + } + + /** + * 移除全部辅助线 + */ + public clearGuideline() { + this.options.canvas.remove( + ...this.options.canvas.getObjects(fabric.GuideLine.prototype.type) + ); + } + + /** + * 显示全部辅助线 + */ + public showGuideline() { + this.options.canvas + .getObjects(fabric.GuideLine.prototype.type) + .forEach(guideLine => { + guideLine.set("visible", true); + }); + this.options.canvas.renderAll(); + } + + /** + * 隐藏全部辅助线 + */ + public hideGuideline() { + this.options.canvas + .getObjects(fabric.GuideLine.prototype.type) + .forEach(guideLine => { + guideLine.set("visible", false); + }); + this.options.canvas.renderAll(); + } + + /** + * 启用 + */ + public enable() { + this.options.enabled = true; + + // 绑定事件 + this.options.canvas.on("after:render", this.eventHandler.calcObjectRect); + this.options.canvas.on("after:render", this.eventHandler.render); + this.options.canvas.on("mouse:down", this.eventHandler.canvasMouseDown); + this.options.canvas.on("mouse:move", this.eventHandler.canvasMouseMove); + this.options.canvas.on("mouse:up", this.eventHandler.canvasMouseUp); + this.options.canvas.on("selection:cleared", this.eventHandler.clearStatus); + + // 显示辅助线 + this.showGuideline(); + + // 绘制一次 + this.render(); + } + + /** + * 禁用 + */ + public disable() { + // 解除事件 + this.options.canvas.off("after:render", this.eventHandler.calcObjectRect); + this.options.canvas.off("after:render", this.eventHandler.render); + this.options.canvas.off("mouse:down", this.eventHandler.canvasMouseDown); + this.options.canvas.off("mouse:move", this.eventHandler.canvasMouseMove); + this.options.canvas.off("mouse:up", this.eventHandler.canvasMouseUp); + this.options.canvas.off("selection:cleared", this.eventHandler.clearStatus); + + // 隐藏辅助线 + this.hideGuideline(); + + this.options.enabled = false; + } + + /** + * 绘制 + */ + public render() { + // if (!this.options.enabled) return; + const vpt = this.options.canvas.viewportTransform; + if (!vpt) return; + // 绘制尺子 + this.draw({ + isHorizontal: true, + rulerLength: this.getSize().width, + // startCalibration: -(vpt[4] / vpt[0]), + startCalibration: this.startCalibration?.x + ? this.startCalibration.x + : -(vpt[4] / vpt[0]) + }); + this.draw({ + isHorizontal: false, + rulerLength: this.getSize().height, + // startCalibration: -(vpt[5] / vpt[3]), + startCalibration: this.startCalibration?.y + ? this.startCalibration.y + : -(vpt[5] / vpt[3]) + }); + // 绘制左上角的遮罩 + drawMask(this.ctx, { + isHorizontal: true, + left: -10, + top: -10, + width: this.options.ruleSize * 2 + 10, + height: this.options.ruleSize + 10, + backgroundColor: this.options.backgroundColor + }); + drawMask(this.ctx, { + isHorizontal: false, + left: -10, + top: -10, + width: this.options.ruleSize + 10, + height: this.options.ruleSize * 2 + 10, + backgroundColor: this.options.backgroundColor + }); + } + + /** + * 获取画板尺寸 + */ + private getSize() { + return { + width: this.options.canvas.width ?? 0, + height: this.options.canvas.height ?? 0 + }; + } + + private getZoom() { + return this.options.canvas.getZoom(); + } + + private draw(opt: { + isHorizontal: boolean; + rulerLength: number; + startCalibration: number; + }) { + const { isHorizontal, rulerLength, startCalibration } = opt; + const zoom = this.getZoom(); + + const gap = getGap(zoom); + const unitLength = rulerLength / zoom; + const startValue = + Math[startCalibration > 0 ? "floor" : "ceil"](startCalibration / gap) * + gap; + const startOffset = startValue - startCalibration; + + // 标尺背景 + const canvasSize = this.getSize(); + darwRect(this.ctx, { + left: 0, + top: 0, + width: isHorizontal ? canvasSize.width : this.options.ruleSize, + height: isHorizontal ? this.options.ruleSize : canvasSize.height, + fill: this.options.backgroundColor, + stroke: this.options.borderColor + }); + + // 颜色 + const textColor = new fabric.Color(this.options.textColor); + // 标尺文字显示 + for (let i = 0; i + startOffset <= Math.ceil(unitLength); i += gap) { + const position = (startOffset + i) * zoom; + const textValue = startValue + i + ""; + const textLength = (10 * textValue.length) / 4; + const textX = isHorizontal + ? position - textLength - 1 + : this.options.ruleSize / 2 - this.options.fontSize / 2 - 4; + const textY = isHorizontal + ? this.options.ruleSize / 2 - this.options.fontSize / 2 - 4 + : position + textLength; + darwText(this.ctx, { + text: textValue, + left: textX, + top: textY, + fill: textColor.toRgb(), + angle: isHorizontal ? 0 : -90 + }); + } + + // 标尺刻度线显示 + for (let j = 0; j + startOffset <= Math.ceil(unitLength); j += gap) { + const position = Math.round((startOffset + j) * zoom); + const left = isHorizontal ? position : this.options.ruleSize - 8; + const top = isHorizontal ? this.options.ruleSize - 8 : position; + const width = isHorizontal ? 0 : 8; + const height = isHorizontal ? 8 : 0; + darwLine(this.ctx, { + left, + top, + width, + height, + stroke: textColor.toRgb() + }); + } + + // 标尺蓝色遮罩 + if (this.objectRect) { + const axis = isHorizontal ? "x" : "y"; + this.objectRect[axis].forEach(rect => { + // 跳过指定矩形 + if (rect.skip === axis) { + return; + } + + // 获取数字的值 + const roundFactor = (x: number) => + Math.round(x / zoom + startCalibration) + ""; + const leftTextVal = roundFactor(isHorizontal ? rect.left : rect.top); + const rightTextVal = roundFactor( + isHorizontal ? rect.left + rect.width : rect.top + rect.height + ); + + const isSameText = leftTextVal === rightTextVal; + + // 背景遮罩 + const maskOpt = { + isHorizontal, + width: isHorizontal ? 160 : this.options.ruleSize - 8, + height: isHorizontal ? this.options.ruleSize - 8 : 160, + backgroundColor: this.options.backgroundColor + }; + drawMask(this.ctx, { + ...maskOpt, + left: isHorizontal ? rect.left - 80 : 0, + top: isHorizontal ? 0 : rect.top - 80 + }); + if (!isSameText) { + drawMask(this.ctx, { + ...maskOpt, + left: isHorizontal ? rect.width + rect.left - 80 : 0, + top: isHorizontal ? 0 : rect.height + rect.top - 80 + }); + } + + // 颜色 + const highlightColor = new fabric.Color(this.options.highlightColor); + + // 高亮遮罩 + highlightColor.setAlpha(0.5); + darwRect(this.ctx, { + left: isHorizontal ? rect.left : this.options.ruleSize - 8, + top: isHorizontal ? this.options.ruleSize - 8 : rect.top, + width: isHorizontal ? rect.width : 8, + height: isHorizontal ? 8 : rect.height, + fill: highlightColor.toRgba() + }); + + // 两边的数字 + const pad = this.options.ruleSize / 2 - this.options.fontSize / 2 - 4; + + const textOpt = { + fill: highlightColor.toRgba(), + angle: isHorizontal ? 0 : -90 + }; + + darwText(this.ctx, { + ...textOpt, + text: leftTextVal, + left: isHorizontal ? rect.left - 2 : pad, + top: isHorizontal ? pad : rect.top - 2, + align: isSameText ? "center" : isHorizontal ? "right" : "left" + }); + + if (!isSameText) { + darwText(this.ctx, { + ...textOpt, + text: rightTextVal, + left: isHorizontal ? rect.left + rect.width + 2 : pad, + top: isHorizontal ? pad : rect.top + rect.height + 2, + align: isHorizontal ? "left" : "right" + }); + } + + // 两边的线 + const lineSize = isSameText ? 8 : 14; + + highlightColor.setAlpha(1); + + const lineOpt = { + width: isHorizontal ? 0 : lineSize, + height: isHorizontal ? lineSize : 0, + stroke: highlightColor.toRgba() + }; + + darwLine(this.ctx, { + ...lineOpt, + left: isHorizontal ? rect.left : this.options.ruleSize - lineSize, + top: isHorizontal ? this.options.ruleSize - lineSize : rect.top + }); + + if (!isSameText) { + darwLine(this.ctx, { + ...lineOpt, + left: isHorizontal + ? rect.left + rect.width + : this.options.ruleSize - lineSize, + top: isHorizontal + ? this.options.ruleSize - lineSize + : rect.top + rect.height + }); + } + }); + } + // draw end + } + + /** + * 计算起始点 + */ + // private calcCalibration() { + // if (this.startCalibration) return; + // // console.log('calcCalibration'); + // const workspace = this.options.canvas.getObjects().find((item: any) => { + // return item.id === 'workspace'; + // }); + // if (!workspace) return; + // const rect = workspace.getBoundingRect(false); + // this.startCalibration = new fabric.Point(-rect.left, -rect.top).divide(this.getZoom()); + // } + + private calcObjectRect() { + const activeObjects = this.options.canvas.getActiveObjects(); + if (activeObjects.length === 0) return; + const allRect = activeObjects.reduce((rects, obj) => { + const rect: HighlightRect = obj.getBoundingRect(false, true); + // 如果是分组单独计算坐标 + if (obj.group) { + const group = { + top: 0, + left: 0, + width: 0, + height: 0, + scaleX: 1, + scaleY: 1, + ...obj.group + }; + // 计算矩形坐标 + rect.width *= group.scaleX; + rect.height *= group.scaleY; + const groupCenterX = group.width / 2 + group.left; + const objectOffsetFromCenterX = + (group.width / 2 + (obj.left ?? 0)) * (1 - group.scaleX); + rect.left += (groupCenterX - objectOffsetFromCenterX) * this.getZoom(); + const groupCenterY = group.height / 2 + group.top; + const objectOffsetFromCenterY = + (group.height / 2 + (obj.top ?? 0)) * (1 - group.scaleY); + rect.top += (groupCenterY - objectOffsetFromCenterY) * this.getZoom(); + } + if (obj instanceof fabric.GuideLine) { + rect.skip = obj.isHorizontal() ? "x" : "y"; + } + rects.push(rect); + return rects; + }, [] as HighlightRect[]); + if (allRect.length === 0) return; + this.objectRect = { + x: mergeLines(allRect, true), + y: mergeLines(allRect, false) + }; + } + + /** + * 清除起始点和矩形坐标 + */ + private clearStatus() { + // this.startCalibration = undefined; + this.objectRect = undefined; + } + + /** + 判断鼠标是否在标尺上 + * @param point + * @returns "vertical" | "horizontal" | false + */ + public isPointOnRuler(point: Point) { + if ( + new fabric.Rect({ + left: 0, + top: 0, + width: this.options.ruleSize, + height: this.options.canvas.height + }).containsPoint(point) + ) { + return "vertical"; + } else if ( + new fabric.Rect({ + left: 0, + top: 0, + width: this.options.canvas.width, + height: this.options.ruleSize + }).containsPoint(point) + ) { + return "horizontal"; + } + return false; + } + + private canvasMouseDown(e: IEvent) { + if (!e.pointer || !e.absolutePointer) return; + const hoveredRuler = this.isPointOnRuler(e.pointer); + if (hoveredRuler && this.activeOn === "up") { + // 备份属性 + this.lastAttr.selection = this.options.canvas.selection; + this.options.canvas.selection = false; + this.activeOn = "down"; + + this.tempGuidelLine = new fabric.GuideLine( + hoveredRuler === "horizontal" + ? e.absolutePointer.y + : e.absolutePointer.x, + { + axis: hoveredRuler, + visible: false + } + ); + + this.options.canvas.add(this.tempGuidelLine); + this.options.canvas.setActiveObject(this.tempGuidelLine); + + this.options.canvas._setupCurrentTransform( + e.e, + this.tempGuidelLine, + true + ); + + this.tempGuidelLine.fire("down", this.getCommonEventInfo(e)); + } + } + + private getCommonEventInfo = (e: IEvent) => { + if (!this.tempGuidelLine || !e.absolutePointer) return; + return { + e: e.e, + transform: this.tempGuidelLine.get("transform"), + pointer: { + x: e.absolutePointer.x, + y: e.absolutePointer.y + }, + target: this.tempGuidelLine + }; + }; + + private canvasMouseMove(e: IEvent) { + if (!e.pointer) return; + + if (this.tempGuidelLine && e.absolutePointer) { + const pos: Partial = {}; + if (this.tempGuidelLine.axis === "horizontal") { + pos.top = e.absolutePointer.y; + } else { + pos.left = e.absolutePointer.x; + } + this.tempGuidelLine.set({ ...pos, visible: true }); + + this.options.canvas.requestRenderAll(); + + const event = this.getCommonEventInfo(e); + this.options.canvas.fire("object:moving", event); + this.tempGuidelLine.fire("moving", event); + } + + const hoveredRuler = this.isPointOnRuler(e.pointer); + if (!hoveredRuler) { + // 鼠标从里面出去 + if (this.lastAttr.status !== "out") { + // 更改鼠标指针 + this.options.canvas.defaultCursor = this.lastAttr.cursor; + this.lastAttr.status = "out"; + } + return; + } + // const activeObjects = this.options.canvas.getActiveObjects(); + // if (activeObjects.length === 1 && activeObjects[0] instanceof fabric.GuideLine) { + // return; + // } + // 鼠标从外边进入 或 在另一侧标尺 + if ( + this.lastAttr.status === "out" || + hoveredRuler !== this.lastAttr.status + ) { + // 更改鼠标指针 + this.lastAttr.cursor = this.options.canvas.defaultCursor; + this.options.canvas.defaultCursor = + hoveredRuler === "horizontal" ? "ns-resize" : "ew-resize"; + this.lastAttr.status = hoveredRuler; + } + } + + private canvasMouseUp(e: IEvent) { + if (this.activeOn !== "down") return; + + // 还原属性 + this.options.canvas.selection = this.lastAttr.selection; + this.activeOn = "up"; + + this.tempGuidelLine?.fire("up", this.getCommonEventInfo(e)); + + this.tempGuidelLine = undefined; + } +} + +export default CanvasRuler; diff --git a/src/core/ruler/type.d.ts b/src/core/ruler/type.d.ts new file mode 100644 index 0000000..a4771cb --- /dev/null +++ b/src/core/ruler/type.d.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type CanvasRuler, { Rect } from "./ruler"; + +declare module "fabric/fabric-impl" { + type EventNameExt = "removed" | EventName; + + export interface Canvas { + _setupCurrentTransform( + e: Event, + target: fabric.Object, + alreadySelected: boolean + ): void; + } + + export interface IObservable { + on( + eventName: "guideline:moving" | "guideline:mouseup", + handler: (event: { e: Event; target: fabric.GuideLine }) => void + ): T; + on(events: { + [key: EventName]: (event: { e: Event; target: fabric.GuideLine }) => void; + }): T; + } + + export interface IGuideLineOptions extends ILineOptions { + axis: "horizontal" | "vertical"; + } + + export interface IGuideLineClassOptions extends IGuideLineOptions { + canvas: { + setActiveObject( + object: fabric.Object | fabric.GuideLine, + e?: Event + ): Canvas; + remove(...object: (fabric.Object | fabric.GuideLine)[]): T; + } & Canvas; + activeOn: "down" | "up"; + initialize(xy: number, objObjects: IGuideLineOptions): void; + callSuper(methodName: string, ...args: unknown[]): any; + getBoundingRect(absolute?: boolean, calculate?: boolean): Rect; + on(eventName: EventNameExt, handler: (e: IEvent) => void): void; + off( + eventName: EventNameExt, + handler?: (e: IEvent) => void + ): void; + fire(eventName: EventNameExt, options?: any): T; + isPointOnRuler(e: MouseEvent): "horizontal" | "vertical" | false; + bringToFront(): fabric.Object; + isHorizontal(): boolean; + } + + export interface GuideLine extends Line, IGuideLineClassOptions {} + + export class GuideLine extends Line { + constructor(xy: number, objObjects?: IGuideLineOptions); + static fromObject(object: any, callback: any): void; + } + + export interface StaticCanvas { + ruler: InstanceType; + } +} diff --git a/src/core/ruler/utils.ts b/src/core/ruler/utils.ts new file mode 100644 index 0000000..89a2987 --- /dev/null +++ b/src/core/ruler/utils.ts @@ -0,0 +1,162 @@ +import type { Rect } from "./ruler"; +import { fabric } from "fabric"; + +/** + * 计算尺子间距 + * @param zoom 缩放比例 + * @returns 返回计算出的尺子间距 + */ +const getGap = (zoom: number) => { + const zooms = [0.02, 0.03, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 18]; + const gaps = [5000, 2500, 1000, 500, 250, 100, 50, 25, 10, 5, 2]; + + let i = 0; + while (i < zooms.length && zooms[i] < zoom) { + i++; + } + + return gaps[i - 1] || 5000; +}; + +/** + * 线段合并 + * @param rect Rect数组 + * @param isHorizontal + * @returns 合并后的Rect数组 + */ +const mergeLines = (rect: Rect[], isHorizontal: boolean) => { + const axis = isHorizontal ? "left" : "top"; + const length = isHorizontal ? "width" : "height"; + // 先按照 axis 的大小排序 + rect.sort((a, b) => a[axis] - b[axis]); + const mergedLines = []; + let currentLine = Object.assign({}, rect[0]); + for (const item of rect) { + const line = Object.assign({}, item); + if (currentLine[axis] + currentLine[length] >= line[axis]) { + // 当前线段和下一个线段相交,合并宽度 + currentLine[length] = + Math.max( + currentLine[axis] + currentLine[length], + line[axis] + line[length] + ) - currentLine[axis]; + } else { + // 当前线段和下一个线段不相交,将当前线段加入结果数组中,并更新当前线段为下一个线段 + mergedLines.push(currentLine); + currentLine = Object.assign({}, line); + } + } + // 加入数组 + mergedLines.push(currentLine); + return mergedLines; +}; + +const darwLine = ( + ctx: CanvasRenderingContext2D, + options: { + left: number; + top: number; + width: number; + height: number; + stroke?: string | CanvasGradient | CanvasPattern; + lineWidth?: number; + } +) => { + ctx.save(); + const { left, top, width, height, stroke, lineWidth } = options; + ctx.beginPath(); + stroke && (ctx.strokeStyle = stroke); + ctx.lineWidth = lineWidth ?? 1; + ctx.moveTo(left, top); + ctx.lineTo(left + width, top + height); + ctx.stroke(); + ctx.restore(); +}; + +const darwText = ( + ctx: CanvasRenderingContext2D, + options: { + left: number; + top: number; + text: string; + fill?: string | CanvasGradient | CanvasPattern; + align?: CanvasTextAlign; + angle?: number; + fontSize?: number; + } +) => { + ctx.save(); + const { left, top, text, fill, align, angle, fontSize } = options; + fill && (ctx.fillStyle = fill); + ctx.textAlign = align ?? "left"; + ctx.textBaseline = "top"; + ctx.font = `${fontSize ?? 10}px sans-serif`; + if (angle) { + ctx.translate(left, top); + ctx.rotate((Math.PI / 180) * angle); + ctx.translate(-left, -top); + } + ctx.fillText(text, left, top); + ctx.restore(); +}; + +const darwRect = ( + ctx: CanvasRenderingContext2D, + options: { + left: number; + top: number; + width: number; + height: number; + fill?: string | CanvasGradient | CanvasPattern; + stroke?: string; + strokeWidth?: number; + } +) => { + ctx.save(); + const { left, top, width, height, fill, stroke, strokeWidth } = options; + ctx.beginPath(); + fill && (ctx.fillStyle = fill); + ctx.rect(left, top, width, height); + ctx.fill(); + if (stroke) { + ctx.strokeStyle = stroke; + ctx.lineWidth = strokeWidth ?? 1; + ctx.stroke(); + } + ctx.restore(); +}; + +const drawMask = ( + ctx: CanvasRenderingContext2D, + options: { + isHorizontal: boolean; + left: number; + top: number; + width: number; + height: number; + backgroundColor: string; + } +) => { + ctx.save(); + const { isHorizontal, left, top, width, height, backgroundColor } = options; + // 创建一个线性渐变对象 + const gradient = isHorizontal + ? ctx.createLinearGradient(left, height / 2, left + width, height / 2) + : ctx.createLinearGradient(width / 2, top, width / 2, height + top); + const transparentColor = new fabric.Color(backgroundColor); + transparentColor.setAlpha(0); + gradient.addColorStop(0, transparentColor.toRgba()); + gradient.addColorStop(0.33, backgroundColor); + gradient.addColorStop(0.67, backgroundColor); + gradient.addColorStop(1, transparentColor.toRgba()); + darwRect(ctx, { + left, + top, + width, + height, + fill: gradient + }); + ctx.restore(); +}; + +export { getGap, mergeLines, darwRect, darwText, darwLine, drawMask }; diff --git a/src/hooks/config.ts b/src/hooks/config.ts new file mode 100644 index 0000000..18adef9 --- /dev/null +++ b/src/hooks/config.ts @@ -0,0 +1,8 @@ +export const customJsonAttr: string[] = [ + "id", + "gradientAngle", + "selectable", + "hasControls", + "userProperty", + "animation" // 动画 +]; diff --git a/src/hooks/handlers/CustomHandler.ts b/src/hooks/handlers/CustomHandler.ts new file mode 100644 index 0000000..e938f7a --- /dev/null +++ b/src/hooks/handlers/CustomHandler.ts @@ -0,0 +1,24 @@ +/* + * @Author: zhoux zhouxia@supervision.ltd + * @Date: 2023-11-30 16:01:52 + * @LastEditors: zhoux zhouxia@supervision.ltd + * @LastEditTime: 2023-11-30 16:02:47 + * @FilePath: \vue-fabric-editor\src\hooks\handlers\CustomHandler.ts + * @Description: 自定义处理器 + */ +import { Handler } from "./Handler"; + +class CustomHandler { + handler: Handler; + + constructor(handler: Handler) { + this.handler = handler; + this.initialze(); + } + + protected initialze() { + // empty + } +} + +export default CustomHandler; diff --git a/src/hooks/handlers/Handler.ts b/src/hooks/handlers/Handler.ts new file mode 100644 index 0000000..8898881 --- /dev/null +++ b/src/hooks/handlers/Handler.ts @@ -0,0 +1,299 @@ +/* + * @Author: zhoux zhouxia@supervision.ltd + * @Date: 2023-11-30 15:59:04 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:06:57 + * @FilePath: \vue-fabric-editor\src\hooks\handlers\Handler.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ + +// import AnimationHandler from './AnimationHandler'; +import CustomHandler from "./CustomHandler"; +import { FabricCanvas, FabricObject, FabricObjects } from "./typing"; + +export interface HandlerCallback { + /** + * When has been added object in Canvas, Called function + * + */ + onAdd?: (object: FabricObject) => void; + /** + * Return contextmenu element + * + */ + // onContext?: (el: HTMLDivElement, e: React.MouseEvent, target?: FabricObject) => Promise | any; + /** + * Return tooltip element + * + */ + onTooltip?: (el: HTMLDivElement, target?: FabricObject) => Promise | any; + /** + * When zoom, Called function + */ + onZoom?: (zoomRatio: number) => void; + /** + * When clicked object, Called function + * + */ + onClick?: (canvas: FabricCanvas, target: FabricObject) => void; + /** + * When double clicked object, Called function + * + */ + onDblClick?: (canvas: FabricCanvas, target: FabricObject) => void; + /** + * When modified object, Called function + */ + onModified?: (target: FabricObject) => void; + /** + * When select object, Called function + * + */ + onSelect?: (target: FabricObject) => void; + /** + * When has been removed object in Canvas, Called function + * + */ + onRemove?: (target: FabricObject) => void; + /** + * When has been undo or redo, Called function + * + */ + // onTransaction?: (transaction: TransactionEvent) => void; + /** + * When has been changed interaction mode, Called function + * + */ + // onInteraction?: (interactionMode: InteractionMode) => void; + /** + * When canvas has been loaded + * + */ + onLoad?: (handler: Handler, canvas?: fabric.Canvas) => void; +} + +export interface HandlerOption { + /** + * Canvas id + * @type {string} + */ + id?: string; + /** + * Canvas object + * @type {FabricCanvas} + */ + canvas?: FabricCanvas; + /** + * Canvas parent element + * @type {HTMLDivElement} + */ + container?: HTMLDivElement; + /** + * Canvas editable + * @type {boolean} + */ + editable?: boolean; + /** + * Canvas interaction mode + * @type {InteractionMode} + */ + // interactionMode?: InteractionMode; + /** + * Persist properties for object + * @type {string[]} + */ + propertiesToInclude?: string[]; + /** + * Minimum zoom ratio + * @type {number} + */ + minZoom?: number; + /** + * Maximum zoom ratio + * @type {number} + */ + maxZoom?: number; + /** + * Zoom ratio step + * @type {number} + */ + zoomStep?: number; + /** + * Workarea option + * @type {WorkareaOption} + */ + // workareaOption?: WorkareaOption; + /** + * Canvas option + * @type {CanvasOption} + */ + // canvasOption?: CanvasOption; + /** + * Grid option + * @type {GridOption} + */ + // gridOption?: GridOption; + /** + * Default option for Fabric Object + * @type {FabricObjectOption} + */ + // objectOption?: FabricObjectOption; + /** + * Guideline option + * @type {GuidelineOption} + */ + // guidelineOption?: GuidelineOption; + /** + * Whether to use zoom + * @type {boolean} + */ + zoomEnabled?: boolean; + /** + * ActiveSelection option + * @type {Partial>} + */ + // activeSelectionOption?: Partial>; + /** + * Canvas width + * @type {number} + */ + width?: number; + /** + * Canvas height + * @type {number} + */ + height?: number; + /** + * Keyboard event in Canvas + * @type {KeyEvent} + */ + // keyEvent?: KeyEvent; + /** + * Append custom objects + * @type {{ [key: string]: any }} + */ + fabricObjects?: FabricObjects; + handlers?: { [key: string]: CustomHandler }; + [key: string]: any; +} + +export type HandlerOptions = HandlerOption & HandlerCallback; + +class Handler implements HandlerOptions { + public id: string | undefined; + public canvas: FabricCanvas | undefined; + + public onAdd?: (object: FabricObject) => void; + // public onContext?: ( + // el: HTMLDivElement, + // e: React.MouseEvent, + // target?: FabricObject + // ) => Promise; + public onTooltip?: ( + el: HTMLDivElement, + target?: FabricObject + ) => Promise; + public onZoom?: (zoomRatio: number) => void; + public onClick?: (canvas: FabricCanvas, target: FabricObject) => void; + public onDblClick?: (canvas: FabricCanvas, target: FabricObject) => void; + public onModified?: (target: FabricObject) => void; + public onSelect?: (target: FabricObject) => void; + public onRemove?: (target: FabricObject) => void; + // public onTransaction?: (transaction: TransactionEvent) => void; + // public onInteraction?: (interactionMode: InteractionMode) => void; + public onLoad?: (handler: Handler, canvas?: fabric.Canvas) => void; + + // public animationHandler!: AnimationHandler; + + constructor(options: HandlerOptions) { + this.initialize(options); + } + + /** + * Initialize handler + * + * @author salgum1114 + * @param {HandlerOptions} options + */ + public initialize(options: HandlerOptions) { + this.initOption(options); + this.initCallback(options); + this.initHandler(); + } + + /** + * Init class fields + * @param {HandlerOptions} options + */ + public initOption = (options: HandlerOptions) => { + this.id = options.id; + this.canvas = options.canvas; + // this.container = options.container; + // this.editable = options.editable; + // this.interactionMode = options.interactionMode; + // this.minZoom = options.minZoom; + // this.maxZoom = options.maxZoom; + // this.zoomStep = options.zoomStep || 0.05; + // this.zoomEnabled = options.zoomEnabled; + // this.width = options.width; + // this.height = options.height; + // this.objects = []; + // this.setPropertiesToInclude(options.propertiesToInclude); + // this.setWorkareaOption(options.workareaOption); + // this.setCanvasOption(options.canvasOption); + // this.setGridOption(options.gridOption); + // this.setObjectOption(options.objectOption); + // this.setFabricObjects(options.fabricObjects); + // this.setGuidelineOption(options.guidelineOption); + // this.setActiveSelectionOption(options.activeSelectionOption); + // this.setKeyEvent(options.keyEvent); + }; + + /** + * Initialize callback + * @param {HandlerOptions} options + */ + public initCallback = (options: HandlerOptions) => { + this.onAdd = options.onAdd; + this.onTooltip = options.onTooltip; + this.onZoom = options.onZoom; + // this.onContext = options.onContext; + this.onClick = options.onClick; + this.onModified = options.onModified; + this.onDblClick = options.onDblClick; + this.onSelect = options.onSelect; + this.onRemove = options.onRemove; + // this.onTransaction = options.onTransaction; + // this.onInteraction = options.onInteraction; + this.onLoad = options.onLoad; + }; + + /** + * Initialize handlers + * + */ + public initHandler = () => { + // this.animationHandler = new AnimationHandler(this); + }; + + /** + * Find object by id + * @param {string} id + * @returns {(FabricObject | null)} + */ + public findById = (id: string): FabricObject | null => { + let findObject; + const exist = this.objects.some(obj => { + if (obj.id === id) { + findObject = obj; + return true; + } + return false; + }); + if (!exist) { + warning(true, "Not found object by id."); + return null; + } + return findObject; + }; +} diff --git a/src/hooks/handlers/typing.ts b/src/hooks/handlers/typing.ts new file mode 100644 index 0000000..ddf04db --- /dev/null +++ b/src/hooks/handlers/typing.ts @@ -0,0 +1,156 @@ +export interface FabricCanvasOption { + wrapperEl?: HTMLElement; +} + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint +export type FabricCanvas = T & + FabricCanvasOption; + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint +export type FabricObjectOption = T & { + /** + * Object id + * @type {string} + */ + id?: string; + /** + * Parent object id + * @type {string} + */ + parentId?: string; + /** + * Original opacity + * @type {number} + */ + originOpacity?: number; + /** + * Original top position + * @type {number} + */ + originTop?: number; + /** + * Original left position + * @type {number} + */ + originLeft?: number; + /** + * Original scale X + * @type {number} + */ + originScaleX?: number; + /** + * Original scale Y + * @type {number} + */ + originScaleY?: number; + /** + * Original angle + * @type {number} + */ + originAngle?: number; + /** + * Original fill color + * + * @type {(string | fabric.Pattern | fabric.Gradient)} + */ + originFill?: string | fabric.Pattern | fabric.Gradient; + /** + * Original stroke color + * @type {string} + */ + originStroke?: string; + /** + * Original rotation + * + * @type {number} + */ + originRotation?: number; + /** + * Object editable + * @type {boolean} + */ + editable?: boolean; + /** + * Object Super type + * @type {string} + */ + superType?: string; + /** + * @description + * @type {string} + */ + description?: string; + /** + * Animation property + * @type {AnimationProperty} + */ + animation?: AnimationProperty; + /** + * Anime instance + * @type {anime.AnimeInstance} + */ + anime?: anime.AnimeInstance; + /** + * Tooltip property + * @type {TooltipProperty} + */ + tooltip?: TooltipProperty; + /** + * Link property + * @type {LinkProperty} + */ + link?: LinkProperty; + /** + * Is running animation + * @type {boolean} + */ + animating?: boolean; + /** + * Object class + * @type {string} + */ + class?: string; + /** + * Is possible delete + * @type {boolean} + */ + deletable?: boolean; + /** + * Is enable double click + * @type {boolean} + */ + dblclick?: boolean; + /** + * Is possible clone + * @type {boolean} + */ + cloneable?: boolean; + /** + * Is locked object + * @type {boolean} + */ + locked?: boolean; + /** + * This property replaces "angle" + * + * @type {number} + */ + rotation?: number; + /** + * Whether it can be clicked + * + * @type {boolean} + */ + clickable?: boolean; + [key: string]: any; +}; + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint +export type FabricObject = T & + FabricObjectOption; + +export type FabricObjects = { + [key: string]: { + create: (...args: any) => FabricObject; + }; +}; diff --git a/src/hooks/select.ts b/src/hooks/select.ts new file mode 100644 index 0000000..e75ed9f --- /dev/null +++ b/src/hooks/select.ts @@ -0,0 +1,72 @@ +/* + * @Author: donghao donghao@supervision.ltd + * @Date: 2024-08-06 16:50:48 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 17:07:30 + * @FilePath: \General-AI-Platform-Web-Client\src\hooks\select.ts + * @Description: useSelect 类型待优化 + */ +import { inject, onBeforeMount, onMounted, reactive } from "vue"; +import { SelectEvent, SelectMode, SelectOneType } from "@/utils/event/types"; + +interface Selector { + mSelectMode: SelectMode; + mSelectOneType: SelectOneType; + mSelectId: string[] | ""; + mSelectIds: string[]; + mSelectActive: unknown[]; +} + +export default function useSelect() { + const state = reactive({ + mSelectMode: SelectMode.EMPTY, + mSelectOneType: SelectOneType.EMPTY, + mSelectId: "", // 选择id + mSelectIds: [], // 选择id + mSelectActive: [] + }); + + const fabric = inject("fabric"); + // const canvas = inject('canvas'); + const canvasEditor = inject("canvasEditor"); + const event = inject("event"); + + const selectOne = e => { + state.mSelectMode = SelectMode.ONE; + state.mSelectId = e[0].id; + state.mSelectOneType = e[0].type; + state.mSelectIds = e.map(item => item.id); + }; + + const selectMulti = e => { + state.mSelectMode = SelectMode.MULTI; + state.mSelectId = ""; + state.mSelectIds = e.map(item => item.id); + }; + + const selectCancel = () => { + state.mSelectId = ""; + state.mSelectIds = []; + state.mSelectMode = SelectMode.EMPTY; + state.mSelectOneType = SelectOneType.EMPTY; + }; + + onMounted(() => { + event.on(SelectEvent.ONE, selectOne); + event.on(SelectEvent.MULTI, selectMulti); + event.on(SelectEvent.CANCEL, selectCancel); + }); + + onBeforeMount(() => { + event.off(SelectEvent.ONE, selectOne); + event.off(SelectEvent.MULTI, selectMulti); + event.off(SelectEvent.CANCEL, selectCancel); + }); + + return { + fabric, + // canvas, + canvasEditor, + mixinState: state + }; +} diff --git a/src/layout/components/tag/index.vue b/src/layout/components/tag/index.vue index f2464fe..d3862e1 100644 --- a/src/layout/components/tag/index.vue +++ b/src/layout/components/tag/index.vue @@ -517,7 +517,7 @@ onBeforeUnmount(() => {
-
+
{ - app.use(router); + app.use(router).use(VueLazyLoad, {}); await router.isReady(); injectResponsiveStorage(app, config); setupStore(app); diff --git a/src/pages/dataScreen/views/modelList/components/ModelBox.vue b/src/pages/dataScreen/views/modelList/components/ModelBox.vue index 6a5f7ce..d15d0b0 100644 --- a/src/pages/dataScreen/views/modelList/components/ModelBox.vue +++ b/src/pages/dataScreen/views/modelList/components/ModelBox.vue @@ -140,7 +140,7 @@ const handleClickDetail = (modelData: CardProductType) => { rgba(255, 207, 95, 0.4) 0%, rgba(255, 207, 95, 0) 100% ); - background-image: url("@/assets/dataScreen/modelList/inUsed.svg"); + // background-image: url("@/assets/dataScreen/modelList/inUsedBg.svg"); background-repeat: no-repeat; cursor: pointer; .model-box-state { diff --git a/src/utils/event/notifier.ts b/src/utils/event/notifier.ts new file mode 100644 index 0000000..8961428 --- /dev/null +++ b/src/utils/event/notifier.ts @@ -0,0 +1,54 @@ +/* + * @Author: 秦少卫 + * @Date: 2022-09-03 19:16:55 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 16:44:50 + * @Description: 自定义事件 + */ + +import EventEmitter from "events"; +import { fabric } from "fabric"; +import { Canvas } from "fabric/fabric-impl"; +import { SelectEvent } from "@/utils/event/types"; + +/** + * 发布订阅器 + */ +class CanvasEventEmitter extends EventEmitter { + handler: Canvas | undefined; + mSelectMode = ""; + + init(handler: CanvasEventEmitter["handler"]) { + this.handler = handler; + if (this.handler) { + this.handler.on("selection:created", () => this.selected()); + this.handler.on("selection:updated", () => this.selected()); + this.handler.on("selection:cleared", () => this.selected()); + } + } + + /** + * 暴露单选多选事件 + * @private + */ + private selected() { + if (!this.handler) { + throw TypeError("还未初始化"); + } + + const actives = this.handler + .getActiveObjects() + .filter(item => !(item instanceof fabric.GuideLine)); // 过滤掉辅助线 + if (actives && actives.length === 1) { + this.emit(SelectEvent.ONE, actives); + } else if (actives && actives.length > 1) { + this.mSelectMode = "multiple"; + this.emit(SelectEvent.MULTI, actives); + } else { + this.emit(SelectEvent.CANCEL); + } + } +} + +export default new CanvasEventEmitter(); +export { CanvasEventEmitter }; diff --git a/src/utils/event/types.ts b/src/utils/event/types.ts new file mode 100644 index 0000000..1db09fe --- /dev/null +++ b/src/utils/event/types.ts @@ -0,0 +1,31 @@ +/* + * @Author: donghao donghao@supervision.ltd + * @Date: 2024-08-06 16:44:42 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-06 16:44:55 + * @FilePath: \General-AI-Platform-Web-Client\src\utils\event\types.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +/* + * @Description: 自定义事件的类型 + */ + +// 选择模式 +export enum SelectMode { + EMPTY = "", + ONE = "one", + MULTI = "multiple" +} + +export enum SelectOneType { + EMPTY = "", + GROUP = "group", + POLYGON = "polygon" +} + +// 选择事件(用于广播) +export enum SelectEvent { + ONE = "selectOne", + MULTI = "selectMultiple", + CANCEL = "selectCancel" +} diff --git a/src/utils/local.ts b/src/utils/local.ts new file mode 100644 index 0000000..8c9296c --- /dev/null +++ b/src/utils/local.ts @@ -0,0 +1,36 @@ +/** + * get localStorage 获取本地存储 + * @param { String } key + */ +export function getLocal(key: string) { + if (!key) throw new Error("key is empty"); + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : null; +} + +/** + * set localStorage 设置本地存储 + * @param { String } key + * @param value + */ +export function setLocal(key: string, value: unknown) { + if (!key) throw new Error("key is empty"); + if (!value) return; + return localStorage.setItem(key, JSON.stringify(value)); +} + +/** + * remove localStorage 移除某个本地存储 + * @param { String } key + */ +export function removeLocal(key: string) { + if (!key) throw new Error("key is empty"); + return localStorage.removeItem(key); +} + +/** + * clear localStorage 清除本地存储 + */ +export function clearLocal() { + return localStorage.clear(); +} diff --git a/src/utils/math.ts b/src/utils/math.ts new file mode 100644 index 0000000..d727131 --- /dev/null +++ b/src/utils/math.ts @@ -0,0 +1,25 @@ +/** + * 获取多边形顶点坐标 + * @param edges 变数 + * @param radius 半径 + * @returns 坐标数组 + */ +const getPolygonVertices = (edges: number, radius: number) => { + const vertices = []; + const interiorAngle = (Math.PI * 2) / edges; + let rotationAdjustment = -Math.PI / 2; + if (edges % 2 === 0) { + rotationAdjustment += interiorAngle / 2; + } + for (let i = 0; i < edges; i++) { + // 画圆取顶点坐标 + const rad = i * interiorAngle + rotationAdjustment; + vertices.push({ + x: Math.cos(rad) * radius, + y: Math.sin(rad) * radius + }); + } + return vertices; +}; + +export { getPolygonVertices }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..79a3bd9 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,136 @@ +/* + * @Author: 秦少卫 + * @Date: 2022-09-05 22:21:55 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-07 16:29:28 + * @Description: 工具文件 + */ + +// import FontFaceObserver from "fontfaceobserver"; +import { useClipboard, useFileDialog, useBase64 } from "@vueuse/core"; +import { message } from "@/utils/message"; + +interface Font { + type: string; + fontFamily: string; +} + +/** + * @description: 图片文件转字符串 + * @param {Blob|File} file 文件 + * @return {String} + */ +export function getImgStr(file: File | Blob): Promise { + return useBase64(file).promise.value; +} + +/** + * @description: 根据json模板下载字体文件 + * @param {String} str + * @return {Promise} + */ +export function downFontByJSON(str: string) { + // const skipFonts = ["arial", "Microsoft YaHei"]; + // const fontFamilies: string[] = JSON.parse(str) + // .objects.filter( + // (item: Font) => + // // 为text 并且不为包含字体 + // // eslint-disable-next-line implicit-arrow-linebreak + // item.type.includes("text") && !skipFonts.includes(item.fontFamily) + // ) + // .map((item: Font) => item.fontFamily); + const fontFamiliesAll = []; + // TODO 暂时不使用 + // .map(fontName => { + // const font = new FontFaceObserver(fontName); + // return font.load(null, 150000); + // }); + return Promise.all(fontFamiliesAll); +} + +/** + * @description: 选择文件 + * @param {Object} options accept = '', capture = '', multiple = false + * @return {Promise} + */ +export function selectFiles(options: { + accept?: string; + capture?: string; + multiple?: boolean; +}): Promise { + return new Promise(resolve => { + const { onChange, open } = useFileDialog(options); + onChange(files => { + resolve(files); + }); + open(); + }); +} + +/** + * @description: 创建图片元素 + * @param {String} str 图片地址或者base64图片 + * @return {Promise} element 图片元素 + */ +export function insertImgFile(str: string) { + return new Promise(resolve => { + const imgEl = document.createElement("img"); + imgEl.src = str; + // 插入页面 + document.body.appendChild(imgEl); + imgEl.onload = () => { + resolve(imgEl); + }; + }); +} + +/** + * Copying text to the clipboard + * @param source Copy source + * @param options Copy options + * @returns Promise that resolves when the text is copied successfully, or rejects when the copy fails. + */ +export const clipboardText = async ( + source: string, + options?: Parameters[0] +) => { + try { + await useClipboard({ source, ...options }).copy(); + message("复制成功", { type: "success" }); + } catch (error) { + message("复制失败", { type: "error" }); + throw error; + } +}; + +export function fetchArrayByAttrObject(record: Record): any[] { + if (!record) { + return [ + { + key: "key", + value: "value" + } + ]; + } + const finalArr: Record[] = []; + for (const key in record) { + finalArr.push({ + key, + value: record[key] + }); + } + return finalArr; +} + +export function setAttrObjectByArray( + record: Record[] +): Record { + if (Array.isArray(record) && record.length && record[0].key != "key1") { + const currObj = new Object(); + record.map((item: Record) => { + currObj[item?.key || "key"] = item.value; + }); + return currObj; + } + return {}; +} diff --git a/src/views/deviceSetting/components/add.scss b/src/views/deviceSetting/components/add.scss new file mode 100644 index 0000000..b12134d --- /dev/null +++ b/src/views/deviceSetting/components/add.scss @@ -0,0 +1,14 @@ +.deviceSettingAdd_modal_box { + .el-form-item { + margin-right: 0; + } + .el-form-item__label { + font-weight: 500; + font-size: 14px; + color: #333333; + line-height: 16px; + text-align: center; + font-style: normal; + text-transform: none; + } +} diff --git a/src/views/deviceSetting/components/add.vue b/src/views/deviceSetting/components/add.vue new file mode 100644 index 0000000..b7089ba --- /dev/null +++ b/src/views/deviceSetting/components/add.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/views/deviceSetting/components/deviceAttr.vue b/src/views/deviceSetting/components/deviceAttr.vue new file mode 100644 index 0000000..f353365 --- /dev/null +++ b/src/views/deviceSetting/components/deviceAttr.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/src/views/deviceSetting/components/deviceSelect.vue b/src/views/deviceSetting/components/deviceSelect.vue new file mode 100644 index 0000000..a5042ce --- /dev/null +++ b/src/views/deviceSetting/components/deviceSelect.vue @@ -0,0 +1,316 @@ + + + + + + + +../hooks/useWatchModels../hooks/useDeviceObject diff --git a/src/views/deviceSetting/hooks/useDeviceObject.ts b/src/views/deviceSetting/hooks/useDeviceObject.ts new file mode 100644 index 0000000..690f2d0 --- /dev/null +++ b/src/views/deviceSetting/hooks/useDeviceObject.ts @@ -0,0 +1,581 @@ +/** + * @交互说明 + * 1. 初始化选中目标的设备对象 【设备名称、使用图标】 + */ +export const useDeviceObject = () => { + const initDeviceGroupObjects: Record = (record: { + id: string; + value: string; + }) => { + console.log(record, "initDeviceGroupObjects"); + // const { value } = record; + // let watchIconObject = watchIcon2; + // switch (value) { + // case "watchError": + // watchIconObject = watchIcon1; + // break; + // case "watchOnline": + // watchIconObject = watchIcon2; + // break; + // case "watchOutline": + // watchIconObject = watchIcon3; + // break; + // case "watchWarn": + // default: + // watchIconObject = watchIcon4; + // break; + // } + return { + selectable: true, + hasControls: true, + lockUniScaling: true, // 当设置为true,Object将无法被锁定比例进行缩放。默认值为false。 + lockScalingX: true, // 当设置为true,Object水平方向将无法被缩放。默认值为false。 + lockScalingY: true, // 当设置为true,Object垂直方向将无法被缩放。默认值为false。 + lockRotation: true, // 当设置为true,Object的旋转将被锁定。默认值为false。 + type: "group", + version: "5.3.0", + originX: "left", + originY: "top", + left: 20.9866, + top: 16.4716, + width: 131, + height: 66, + fill: "rgb(0,0,0)", + stroke: null, + strokeWidth: 0, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + objects: [ + { + type: "group", + version: "5.3.0", + originX: "left", + originY: "top", + left: -65.5, + top: -33, + width: 131, + height: 66, + fill: "rgb(0,0,0)", + stroke: null, + strokeWidth: 0, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: "", + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + objects: [ + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -60, + top: -31.5, + width: 119, + height: 54, + fill: "white", + stroke: null, + strokeWidth: 1, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "evenodd", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 10, 2], + ["C", 7.79086, 2, 6, 3.79086, 6, 6], + ["L", 6, 44], + ["C", 6, 46.2091, 7.79086, 48, 10, 48], + ["L", 58.8, 48], + ["L", 66, 56], + ["L", 73.2, 48], + ["L", 121, 48], + ["C", 123.209, 48, 125, 46.2091, 125, 44], + ["L", 125, 6], + ["C", 125, 3.79086, 123.209, 2, 121, 2], + ["L", 10, 2], + ["Z"] + ] + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -61, + top: -32.5, + width: 121, + height: 56.4948, + fill: "rgba(21,77,221,0.2)", + stroke: null, + strokeWidth: 1, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 58.8, 48], + ["L", 59.5433, 47.331], + ["L", 59.2454, 47], + ["L", 58.8, 47], + ["L", 58.8, 48], + ["Z"], + ["M", 66, 56], + ["L", 65.2567, 56.669], + ["L", 66, 57.4948], + ["L", 66.7433, 56.669], + ["L", 66, 56], + ["Z"], + ["M", 73.2, 48], + ["L", 73.2, 47], + ["L", 72.7546, 47], + ["L", 72.4567, 47.331], + ["L", 73.2, 48], + ["Z"], + ["M", 7, 6], + ["C", 7, 4.34315, 8.34315, 3, 10, 3], + ["L", 10, 1], + ["C", 7.23858, 1, 5, 3.23857, 5, 6], + ["L", 7, 6], + ["Z"], + ["M", 7, 44], + ["L", 7, 6], + ["L", 5, 6], + ["L", 5, 44], + ["L", 7, 44], + ["Z"], + ["M", 10, 47], + ["C", 8.34315, 47, 7, 45.6569, 7, 44], + ["L", 5, 44], + ["C", 5, 46.7614, 7.23858, 49, 10, 49], + ["L", 10, 47], + ["Z"], + ["M", 58.8, 47], + ["L", 10, 47], + ["L", 10, 49], + ["L", 58.8, 49], + ["L", 58.8, 47], + ["Z"], + ["M", 58.0567, 48.669], + ["L", 65.2567, 56.669], + ["L", 66.7433, 55.331], + ["L", 59.5433, 47.331], + ["L", 58.0567, 48.669], + ["Z"], + ["M", 66.7433, 56.669], + ["L", 73.9433, 48.669], + ["L", 72.4567, 47.331], + ["L", 65.2567, 55.331], + ["L", 66.7433, 56.669], + ["Z"], + ["M", 121, 47], + ["L", 73.2, 47], + ["L", 73.2, 49], + ["L", 121, 49], + ["L", 121, 47], + ["Z"], + ["M", 124, 44], + ["C", 124, 45.6569, 122.657, 47, 121, 47], + ["L", 121, 49], + ["C", 123.761, 49, 126, 46.7614, 126, 44], + ["L", 124, 44], + ["Z"], + ["M", 124, 6], + ["L", 124, 44], + ["L", 126, 44], + ["L", 126, 6], + ["L", 124, 6], + ["Z"], + ["M", 121, 3], + ["C", 122.657, 3, 124, 4.34315, 124, 6], + ["L", 126, 6], + ["C", 126, 3.23858, 123.761, 1, 121, 1], + ["L", 121, 3], + ["Z"], + ["M", 10, 3], + ["L", 121, 3], + ["L", 121, 1], + ["L", 10, 1], + ["L", 10, 3], + ["Z"] + ] + }, + { + type: "rect", + version: "5.3.0", + originX: "left", + originY: "top", + left: -52, + top: -24.5, + width: 32, + height: 32, + fill: "#52C41A", + stroke: null, + strokeWidth: 1, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + rx: 2, + ry: 2, + selectable: true, + hasControls: true + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -45.9945, + top: -17.6, + width: 19.8963, + height: 0, + fill: "", + stroke: "white", + strokeWidth: 1.2, + strokeDashArray: null, + strokeLineCap: "round", + strokeDashOffset: 0, + strokeLineJoin: "round", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 40.0018, 16], + ["L", 29.5301, 16], + ["L", 20.1055, 16] + ] + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -36.5727, + top: -17.6, + width: 0, + height: 8.8419, + fill: "", + stroke: "white", + strokeWidth: 1.2, + strokeDashArray: null, + strokeLineCap: "round", + strokeDashOffset: 0, + strokeLineJoin: "round", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 29.5273, 24.8419], + ["L", 29.5273, 16] + ] + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -46.1, + top: -10.5375, + width: 18.5869, + height: 10.9372, + fill: "white", + stroke: "white", + strokeWidth: 1.2, + strokeDashArray: null, + strokeLineCap: "round", + strokeDashOffset: 0, + strokeLineJoin: "round", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 21.8972, 23.0625], + ["L", 38.5869, 27.5048], + ["L", 37.6746, 28.8773], + ["L", 35.579, 32.6272], + ["L", 34.6667, 33.9997], + ["L", 20, 30.0959], + ["L", 21.8972, 23.0625], + ["Z"] + ] + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -30.5219, + top: -4.7211, + width: 4.1186, + height: 4.5576, + fill: "", + stroke: "white", + strokeWidth: 1.2, + strokeDashArray: null, + strokeLineCap: "round", + strokeDashOffset: 0, + strokeLineJoin: "round", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 37.6737, 28.8789], + ["L", 39.6967, 29.4174], + ["L", 38.6126, 33.4365], + ["L", 35.5781, 32.6288] + ] + }, + { + type: "path", + version: "5.3.0", + originX: "left", + originY: "top", + left: -43.3008, + top: -7.6387, + width: 4.2123, + height: 4.181, + fill: "", + stroke: "#52C41A", + strokeWidth: 0.5, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: null, + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + selectable: true, + hasControls: true, + path: [ + ["M", 26.6615, 27.7018], + ["C", 26.6615, 28.8548, 25.7201, 29.7923, 24.5554, 29.7923], + ["C", 23.3906, 29.7923, 22.4492, 28.8548, 22.4492, 27.7018], + ["C", 22.4492, 26.5488, 23.3906, 25.6113, 24.5554, 25.6113], + ["C", 25.7201, 25.6113, 26.6615, 26.5488, 26.6615, 27.7018], + ["Z"] + ] + } + ] + }, + { + type: "textbox", + version: "5.3.0", + originX: "left", + originY: "top", + left: -15.709, + top: -15.0177, + width: 67.6873, + height: 13.56, + fill: "rgb(0,0,0)", + stroke: null, + strokeWidth: 1, + strokeDashArray: null, + strokeLineCap: "butt", + strokeDashOffset: 0, + strokeLineJoin: "miter", + strokeUniform: false, + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + shadow: "", + visible: true, + backgroundColor: "", + fillRule: "nonzero", + paintFirst: "fill", + globalCompositeOperation: "source-over", + skewX: 0, + skewY: 0, + fontFamily: "arial", + fontWeight: "normal", + fontSize: 12, + text: record?.name || "设备", + underline: false, + overline: false, + linethrough: false, + textAlign: "left", + fontStyle: "normal", + lineHeight: 1.16, + textBackgroundColor: "", + charSpacing: 0, + styles: [], + direction: "ltr", + path: null, + pathStartOffset: 0, + pathSide: "left", + pathAlign: "baseline", + minWidth: 20, + splitByGrapheme: true, + selectable: true, + hasControls: true + } + ] + }; + }; + return { + initDeviceGroupObjects + }; +}; diff --git a/src/views/deviceSetting/hooks/useWatchModels.ts b/src/views/deviceSetting/hooks/useWatchModels.ts new file mode 100644 index 0000000..4ad1cdc --- /dev/null +++ b/src/views/deviceSetting/hooks/useWatchModels.ts @@ -0,0 +1,118 @@ +/* + * @Author: zhoux zhouxia@supervision.ltd + * @Date: 2023-12-13 14:29:17 + * @LastEditors: donghao donghao@supervision.ltd + * @LastEditTime: 2024-08-07 11:31:52 + * @FilePath: \vue-fabric-editor\src\hooks\useAddModels.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +// import {watch00} from '@/assets/modelSettingJsons/watch01' + +import watchError from "@/assets/modelSetting/watchError.svg"; +import watchOnline from "@/assets/modelSetting/watchOnline.svg"; +import watchOutline from "@/assets/modelSetting/watchOutline.svg"; +import watchWarn from "@/assets/modelSetting/watchWarn.svg"; +import watchErrorSelected from "@/assets/modelSetting/watchErrorSelected.svg"; +import watchOnlineSelected from "@/assets/modelSetting/watchOnlineSelected.svg"; +import watchOutlineSelected from "@/assets/modelSetting/watchOutlineSelected.svg"; +import watchWarnSelected from "@/assets/modelSetting/watchWarnSelected.svg"; +export interface materialItemI { + value: string; + name: string; + tempUrl: string; + src: string; + color?: string; +} +const customObjectJson = { + type: "group", + objects: [ + { + type: "rect", + left: 100, + top: 100, + width: 100, + height: 100, + fill: "red" + }, + { + type: "circle", + left: 200, + top: 100, + radius: 50, + fill: "blue" + }, + { + type: "text", + left: 150, + top: 200, + text: "Hello", + fontSize: 24, + fill: "white" + } + ] +}; +export const useWatchModels = () => { + const locaWatchList: materialItemI[] = [ + { + value: "watchError", + name: "摄像头1", + tempUrl: "", + groupObject: customObjectJson, + src: watchError, + color: "#E80D0D" + }, + { + value: "watchOnline", + name: "摄像头", + tempUrl: "", + src: watchOnline, + color: "#52C41A" + }, + { + value: "watchOutline", + name: "摄像头", + tempUrl: "", + src: watchOutline, + color: "#CCCCCC" + }, + { + value: "watchWarn", + name: "摄像头", + tempUrl: "", + src: watchWarn, + color: "#FAAD14" + }, + { + value: "watchErrorSelected", + name: "摄像头", + tempUrl: "", + src: watchErrorSelected, + color: "#E80D0D" + }, + { + value: "watchOnlineSelected", + name: "摄像头", + tempUrl: "", + src: watchOnlineSelected, + color: "#52C41A" + }, + { + value: "watchOutlineSelected", + name: "摄像头", + tempUrl: "", + src: watchOutlineSelected, + color: "#CCCCCC" + }, + { + value: "watchWarnSelected", + name: "摄像头", + tempUrl: "", + src: watchWarnSelected, + color: "#FAAD14" + } + ]; + + return { + locaWatchList + }; +}; diff --git a/src/views/deviceSetting/index.scss b/src/views/deviceSetting/index.scss new file mode 100644 index 0000000..4f112b4 --- /dev/null +++ b/src/views/deviceSetting/index.scss @@ -0,0 +1,53 @@ +.deviceSetting_wrap { + height: 100vh; + background: #fff; + border-radius: 2px; + & > header { + box-sizing: border-box; + padding: 16px; + height: 62px; + font-family: Douyin Sans, Douyin Sans; + font-weight: bold; + font-size: 20px; + color: #333333; + border-bottom: 1px solid rgba(21, 77, 221, 0.2); + } + + .main_content { + /* background: red; */ + height: calc(100vh - 160px); + .device_add_wrap { + width: 57.64vw; + margin: 0 auto; + padding-top: 40px; + .bg_preview { + height: 412px; + background-color: red; + } + } + } + + /* TODO 待使用 */ + .right-bar { + width: 304px; + height: 100%; + padding: 10px; + overflow-y: auto; + background: #fff; + } + #workspace { + flex: 1; + width: 100%; + position: relative; + overflow: hidden; + } + + .content { + flex: 1; + width: 220px; + padding: 10px; + padding-top: 0; + height: 100%; + overflow-y: auto; + } +} diff --git a/src/views/deviceSetting/index.vue b/src/views/deviceSetting/index.vue index 551e83a..45cf941 100644 --- a/src/views/deviceSetting/index.vue +++ b/src/views/deviceSetting/index.vue @@ -1,70 +1,206 @@ - - + + + diff --git a/src/views/deviceSetting/testData/bg01.json b/src/views/deviceSetting/testData/bg01.json new file mode 100644 index 0000000..51eeb38 --- /dev/null +++ b/src/views/deviceSetting/testData/bg01.json @@ -0,0 +1,85 @@ +{ + "version": "5.3.0", + "objects": [ + { + "type": "rect", + "version": "5.3.0", + "originX": "left", + "originY": "top", + "left": 0, + "top": 0, + "width": 1200, + "height": 900, + "fill": "rgba(255,255,255,1)", + "stroke": null, + "strokeWidth": 0, + "strokeDashArray": null, + "strokeLineCap": "butt", + "strokeDashOffset": 0, + "strokeLineJoin": "miter", + "strokeUniform": false, + "strokeMiterLimit": 4, + "scaleX": 1, + "scaleY": 1, + "angle": 0, + "flipX": false, + "flipY": false, + "opacity": 1, + "shadow": null, + "visible": true, + "backgroundColor": "", + "fillRule": "nonzero", + "paintFirst": "fill", + "globalCompositeOperation": "source-over", + "skewX": 0, + "skewY": 0, + "rx": 0, + "ry": 0, + "id": "workspace", + "selectable": false, + "hasControls": false + }, + { + "type": "image", + "version": "5.3.0", + "originX": "left", + "originY": "top", + "left": 23.2346, + "top": 23.0507, + "width": 1600, + "height": 1200, + "fill": "rgb(0,0,0)", + "stroke": null, + "strokeWidth": 0, + "strokeDashArray": null, + "strokeLineCap": "butt", + "strokeDashOffset": 0, + "strokeLineJoin": "miter", + "strokeUniform": false, + "strokeMiterLimit": 4, + "scaleX": 0.721, + "scaleY": 0.721, + "angle": 0, + "flipX": false, + "flipY": false, + "opacity": 1, + "shadow": null, + "visible": true, + "backgroundColor": "", + "fillRule": "nonzero", + "paintFirst": "fill", + "globalCompositeOperation": "source-over", + "skewX": 0, + "skewY": 0, + "cropX": 0, + "cropY": 0, + "id": "a3ab29c6-7008-49fe-abf3-edc9a47cd460", + "selectable": false, + "hasControls": false, + "evented": false, + "crossOrigin": null, + "src": "https://img.cgmodel.com/image/2020/1010/big/1537169-1390622992.jpg", + "filters": [] + } + ] +} diff --git a/src/views/deviceSetting/testData/room2.png b/src/views/deviceSetting/testData/room2.png new file mode 100644 index 0000000..4c23916 Binary files /dev/null and b/src/views/deviceSetting/testData/room2.png differ diff --git a/src/views/deviceSetting/testData/房间平面图 (4).png b/src/views/deviceSetting/testData/房间平面图 (4).png new file mode 100644 index 0000000..1928c87 Binary files /dev/null and b/src/views/deviceSetting/testData/房间平面图 (4).png differ diff --git a/src/views/deviceSetting/testData/房间平面图.png b/src/views/deviceSetting/testData/房间平面图.png new file mode 100644 index 0000000..0a90aa9 Binary files /dev/null and b/src/views/deviceSetting/testData/房间平面图.png differ diff --git a/types/auto-imports.d.ts b/types/auto-imports.d.ts new file mode 100644 index 0000000..d5e7dcf --- /dev/null +++ b/types/auto-imports.d.ts @@ -0,0 +1,78 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: typeof import("vue")["EffectScope"]; + const computed: typeof import("vue")["computed"]; + const createApp: typeof import("vue")["createApp"]; + const customRef: typeof import("vue")["customRef"]; + const defineAsyncComponent: typeof import("vue")["defineAsyncComponent"]; + const defineComponent: typeof import("vue")["defineComponent"]; + const effectScope: typeof import("vue")["effectScope"]; + const getCurrentInstance: typeof import("vue")["getCurrentInstance"]; + const getCurrentScope: typeof import("vue")["getCurrentScope"]; + const h: typeof import("vue")["h"]; + const inject: typeof import("vue")["inject"]; + const isProxy: typeof import("vue")["isProxy"]; + const isReactive: typeof import("vue")["isReactive"]; + const isReadonly: typeof import("vue")["isReadonly"]; + const isRef: typeof import("vue")["isRef"]; + const markRaw: typeof import("vue")["markRaw"]; + const nextTick: typeof import("vue")["nextTick"]; + const onActivated: typeof import("vue")["onActivated"]; + const onBeforeMount: typeof import("vue")["onBeforeMount"]; + const onBeforeUnmount: typeof import("vue")["onBeforeUnmount"]; + const onBeforeUpdate: typeof import("vue")["onBeforeUpdate"]; + const onDeactivated: typeof import("vue")["onDeactivated"]; + const onErrorCaptured: typeof import("vue")["onErrorCaptured"]; + const onMounted: typeof import("vue")["onMounted"]; + const onRenderTracked: typeof import("vue")["onRenderTracked"]; + const onRenderTriggered: typeof import("vue")["onRenderTriggered"]; + const onScopeDispose: typeof import("vue")["onScopeDispose"]; + const onServerPrefetch: typeof import("vue")["onServerPrefetch"]; + const onUnmounted: typeof import("vue")["onUnmounted"]; + const onUpdated: typeof import("vue")["onUpdated"]; + const provide: typeof import("vue")["provide"]; + const reactive: typeof import("vue")["reactive"]; + const readonly: typeof import("vue")["readonly"]; + const ref: typeof import("vue")["ref"]; + const resolveComponent: typeof import("vue")["resolveComponent"]; + const shallowReactive: typeof import("vue")["shallowReactive"]; + const shallowReadonly: typeof import("vue")["shallowReadonly"]; + const shallowRef: typeof import("vue")["shallowRef"]; + const toRaw: typeof import("vue")["toRaw"]; + const toRef: typeof import("vue")["toRef"]; + const toRefs: typeof import("vue")["toRefs"]; + const toValue: typeof import("vue")["toValue"]; + const triggerRef: typeof import("vue")["triggerRef"]; + const unref: typeof import("vue")["unref"]; + const useAttrs: typeof import("vue")["useAttrs"]; + const useCssModule: typeof import("vue")["useCssModule"]; + const useCssVars: typeof import("vue")["useCssVars"]; + const useSlots: typeof import("vue")["useSlots"]; + const watch: typeof import("vue")["watch"]; + const watchEffect: typeof import("vue")["watchEffect"]; + const watchPostEffect: typeof import("vue")["watchPostEffect"]; + const watchSyncEffect: typeof import("vue")["watchSyncEffect"]; +} +// for type re-export +declare global { + // @ts-ignore + export type { + Component, + ComponentPublicInstance, + ComputedRef, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + VNode, + WritableComputedRef + } from "vue"; + import("vue"); +} diff --git a/types/editor.d.ts b/types/editor.d.ts new file mode 100644 index 0000000..7d3a7ef --- /dev/null +++ b/types/editor.d.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * @Author: 秦少卫 + * @Date: 2023-05-13 18:53:44 + * @LastEditors: 秦少卫 + * @LastEditTime: 2023-06-27 22:44:31 + * @Description: file content + */ + +declare interface IPluginOption { + [propName: string]: unknown; +} + +// 生命周期事件类型 +declare type IEditorHooksType = + | "hookImportBefore" + | "hookImportAfter" + | "hookSaveBefore" + | "hookSaveAfter"; + +// 插件class +declare interface IPluginClass extends IPluginTempl { + new ( + canvas: fabric.Canvas, + editor: any, + options: IPluginOption + ): IPluginTempl; +} + +declare interface IPluginMenu { + text: string; + command?: () => void; + child?: IPluginMenu[]; +} + +// 插件实例 +declare interface IPluginTempl { + pluginName: string; + events: string[]; + apis: string[]; + canvas: fabric.Canvas; + hotkeyEvent: (name: string, e: Event) => viode; + [propName: IEditorHooksType]: () => void; + [propName: string]: any; +} diff --git a/types/env.d.ts b/types/env.d.ts new file mode 100644 index 0000000..74b8d34 --- /dev/null +++ b/types/env.d.ts @@ -0,0 +1,30 @@ +/// + +// import { Object } from 'fabric/fabric-impl'; + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any>; + export default component; +} + +declare global { + declare module "fabric/fabric-impl" { + interface IObjectOptions { + /** + * 标识 + */ + id?: string | undefined; + } + } +} + +export as namespace vfe; +declare module "vfe" { + export as namespace vfe; + export interface ICanvas extends fabric.Canvas { + c: fabric.Canvas; + editor: Editor; + } +} diff --git a/types/extends.d.ts b/types/extends.d.ts new file mode 100644 index 0000000..db6c37e --- /dev/null +++ b/types/extends.d.ts @@ -0,0 +1,35 @@ +declare namespace fabric { + export interface Canvas { + contextTop: CanvasRenderingContext2D; + lowerCanvasEl: HTMLElement; + _currentTransform: unknown; + _centerObject: (obj: fabric.Object, center: fabric.Point) => fabric.Canvas; + } + + export interface Control { + rotate: number; + } + + function ControlMouseEventHandler( + eventData: MouseEvent, + transformData: Transform, + x: number, + y: number + ): boolean; + function ControlStringHandler( + eventData: MouseEvent, + control: fabric.Control, + fabricObject: fabric.Object + ): string; + export const controlsUtils: { + rotationWithSnapping: ControlMouseEventHandler; + scalingEqually: ControlMouseEventHandler; + scalingYOrSkewingX: ControlMouseEventHandler; + scalingXOrSkewingY: ControlMouseEventHandler; + + scaleCursorStyleHandler: ControlStringHandler; + scaleSkewCursorStyleHandler: ControlStringHandler; + scaleOrSkewActionName: ControlStringHandler; + rotationStyleHandler: ControlStringHandler; + }; +} diff --git a/vite.config.ts b/vite.config.ts index aac4625..053c024 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,7 @@ * @Author: donghao donghao@supervision.ltd * @Date: 2024-02-22 13:38:05 * @LastEditors: donghao donghao@supervision.ltd - * @LastEditTime: 2024-02-22 14:18:31 + * @LastEditTime: 2024-08-07 17:09:15 * @FilePath: \General-AI-Platform-Web-Client\vite.config.ts * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ @@ -13,7 +13,7 @@ import { warpperEnv } from "./build"; import { getPluginsList } from "./build/plugins"; import { include, exclude } from "./build/optimize"; import { UserConfigExport, ConfigEnv, loadEnv } from "vite"; - +import autoImports from "unplugin-auto-import/vite"; /** 当前执行node命令时文件夹的地址(工作目录) */ const root: string = process.cwd(); @@ -23,10 +23,10 @@ const pathResolve = (dir: string): string => { }; /** 设置别名 */ -const alias: Record = { - "@": pathResolve("src"), - "@build": pathResolve("build") -}; +// const alias: Record = { +// "@": pathResolve("src"), +// "@build": pathResolve("build") +// }; const { dependencies, devDependencies, name, version } = pkg; const __APP_INFO__ = { @@ -41,7 +41,11 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => { base: VITE_PUBLIC_PATH, root, resolve: { - alias + alias: [ + { find: /^@\//, replacement: resolve(__dirname, "src") + "/" }, + { find: /^~/, replacement: "" }, + { find: /^@build\//, replacement: pathResolve("build") } + ] }, // 服务端渲染 server: { @@ -60,7 +64,16 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => { } } }, - plugins: getPluginsList(command, VITE_CDN, VITE_COMPRESSION), + plugins: [ + getPluginsList(command, VITE_CDN, VITE_COMPRESSION), + autoImports({ + imports: ["vue"], + eslintrc: { + enabled: true + } + }) + ], + // https://cn.vitejs.dev/config/dep-optimization-options.html#dep-optimization-options optimizeDeps: { include,