feat: add resizer and add zoom in / out for canvas

This commit is contained in:
SikongJueluo 2025-06-30 21:39:08 +08:00
parent 8207c37e12
commit 6cf7ef02ac
No known key found for this signature in database
9 changed files with 1205 additions and 448 deletions

50
components.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
Canvas: typeof import('./src/components/Canvas.vue')['default']
CollapsibleSection: typeof import('./src/components/CollapsibleSection.vue')['default']
ComponentSelector: typeof import('./src/components/ComponentSelector.vue')['default']
DDR: typeof import('./src/components/equipments/DDR.vue')['default']
DDS: typeof import('./src/components/equipments/DDS.vue')['default']
DDSPropertyEditor: typeof import('./src/components/equipments/DDSPropertyEditor.vue')['default']
DiagramCanvas: typeof import('./src/components/DiagramCanvas.vue')['default']
Dialog: typeof import('./src/components/Dialog.vue')['default']
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
LabCanvas: typeof import('./src/components/LabCanvas.vue')['default']
LoginCard: typeof import('./src/components/LoginCard.vue')['default']
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']
MotherBoard: typeof import('./src/components/equipments/MotherBoard.vue')['default']
MotherBoardCaps: typeof import('./src/components/equipments/MotherBoardCaps.vue')['default']
Navbar: typeof import('./src/components/Navbar.vue')['default']
PG2L100H_FBG676: typeof import('./src/components/equipments/PG2L100H_FBG676.vue')['default']
Pin: typeof import('./src/components/equipments/Pin.vue')['default']
PopButton: typeof import('./src/components/PopButton.vue')['default']
PropertyEditor: typeof import('./src/components/PropertyEditor.vue')['default']
PropertyPanel: typeof import('./src/components/PropertyPanel.vue')['default']
RekaSplitterGroup: typeof import('reka-ui')['SplitterGroup']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SD: typeof import('./src/components/equipments/SD.vue')['default']
SevenSegmentDisplay: typeof import('./src/components/equipments/SevenSegmentDisplay.vue')['default']
SFP: typeof import('./src/components/equipments/SFP.vue')['default']
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
SMA: typeof import('./src/components/equipments/SMA.vue')['default']
SMT_LED: typeof import('./src/components/equipments/SMT_LED.vue')['default']
Switch: typeof import('./src/components/equipments/Switch.vue')['default']
ThemeControlButton: typeof import('./src/components/ThemeControlButton.vue')['default']
ThemeControlToggle: typeof import('./src/components/ThemeControlToggle.vue')['default']
TutorialCarousel: typeof import('./src/components/TutorialCarousel.vue')['default']
UploadCard: typeof import('./src/components/UploadCard.vue')['default']
Wire: typeof import('./src/components/equipments/Wire.vue')['default']
}
}

615
package-lock.json generated
View File

@ -15,10 +15,11 @@
"konva": "^9.3.20",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"lucide-vue-next": "^0.525.0",
"marked": "^12.0.0",
"mathjs": "^14.4.0",
"pinia": "^3.0.1",
"tinypool": "^1.0.2",
"reka-ui": "^2.3.1",
"ts-log": "^2.2.7",
"ts-results-es": "^5.0.1",
"vue": "^3.5.13",
@ -41,6 +42,7 @@
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"
@ -962,6 +964,86 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@floating-ui/vue": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.7.tgz",
"integrity": "sha512-idmAtbAIigGXN2SI5gItiXYBYtNfDTP9yIiObxgu13dgtG7ARCHlNfnR29GxP4LI4o13oiwsJ8wVgghj1lNqcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.2",
"@floating-ui/utils": "^0.2.10",
"vue-demi": ">=0.13.0"
}
},
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@internationalized/date": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@internationalized/number": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -1354,6 +1436,15 @@
"url": "https://github.com/sponsors/Fuzzyma"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
@ -1622,6 +1713,32 @@
"tailwindcss": "4.1.4"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/vue-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.0.0"
}
},
"node_modules/@tsconfig/node22": {
"version": "22.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz",
@ -1652,6 +1769,12 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
@ -1992,6 +2115,55 @@
}
}
},
"node_modules/@vueuse/core": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "12.8.2",
"@vueuse/shared": "12.8.2",
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/metadata": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
"integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
"integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
"license": "MIT",
"dependencies": {
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@ -2012,6 +2184,45 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
@ -2066,6 +2277,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/birpc": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
@ -2085,6 +2309,19 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@ -2155,6 +2392,31 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/complex.js": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
@ -2168,6 +2430,13 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -2252,9 +2521,9 @@
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2318,6 +2587,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@ -2461,6 +2736,13 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"dev": true,
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
@ -2492,6 +2774,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2563,6 +2858,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -2615,6 +2923,19 @@
"node": ">=18.18.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@ -2631,6 +2952,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@ -2650,6 +2994,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@ -3064,6 +3418,24 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -3096,6 +3468,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-vue-next": {
"version": "0.525.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.525.0.tgz",
"integrity": "sha512-Xf8+x8B2DrnGDV/rxylS+KBp2FIe6ljwDn2JsGTZZvXIfhmm/q+nv8RuGO1OyoMjOVkkz7CqtUqJfwtFPRbB2w==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -3184,6 +3565,38 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
}
},
"node_modules/mlly/node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@ -3233,6 +3646,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
@ -3323,6 +3746,12 @@
"npm": ">=3.10.8"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/open": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
@ -3438,6 +3867,18 @@
}
}
},
"node_modules/pkg-types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.1.tgz",
"integrity": "sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@ -3489,6 +3930,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/read-package-json-fast": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@ -3503,6 +3961,53 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/reka-ui": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.3.1.tgz",
"integrity": "sha512-2SjGeybd7jvD8EQUkzjgg7GdOQdf4cTwdVMq/lDNTMqneUFNnryGO43dg8WaM/jaG9QpSCZBvstfBFWlDdb2Zg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
"@floating-ui/vue": "^1.1.6",
"@internationalized/date": "^3.5.0",
"@internationalized/number": "^3.5.0",
"@tanstack/vue-virtual": "^3.12.0",
"@vueuse/core": "^12.5.0",
"@vueuse/shared": "^12.5.0",
"aria-hidden": "^1.2.4",
"defu": "^6.1.4",
"ohash": "^2.0.11"
},
"peerDependencies": {
"vue": ">= 3.2.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@ -3709,9 +4214,9 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3725,13 +4230,17 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinypool": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": ">=8.0"
}
},
"node_modules/totalist": {
@ -3785,6 +4294,13 @@
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@ -3815,6 +4331,74 @@
"node": ">= 10.0.0"
}
},
"node_modules/unplugin": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz",
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"picomatch": "^4.0.2",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/unplugin-utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.4.tgz",
"integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pathe": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=18.12.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-vue-components": {
"version": "28.8.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz",
"integrity": "sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"debug": "^4.4.1",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.5",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@babel/parser": "^7.15.8",
"@nuxt/kit": "^3.2.2 || ^4.0.0",
"vue": "2 || 3"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@ -4103,6 +4687,13 @@
"typescript": ">=5.0.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",

View File

@ -21,10 +21,11 @@
"konva": "^9.3.20",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"lucide-vue-next": "^0.525.0",
"marked": "^12.0.0",
"mathjs": "^14.4.0",
"pinia": "^3.0.1",
"tinypool": "^1.0.2",
"reka-ui": "^2.3.1",
"ts-log": "^2.2.7",
"ts-results-es": "^5.0.1",
"vue": "^3.5.13",
@ -47,6 +48,7 @@
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"

10
src/assets/base.css Normal file
View File

@ -0,0 +1,10 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));

View File

@ -1,11 +1,4 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
@import "base.css";
/* 禁止所有图像和SVG选择 */
img, svg {

View File

@ -0,0 +1,499 @@
<template>
<div>
<v-stage
class="h-full w-full"
ref="stageRef"
:config="stageSize"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@wheel="handleWheel"
@click="handleStageClick"
>
<v-layer ref="layerRef">
<template
ref="canvasObjects"
v-for="item in objMap.values()"
:key="item.id"
>
<v-group
:config="{
x: item.x,
y: item.y,
draggable: true,
id: `group-${item.id}`,
}"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
@mouseover="handleCanvasObjectMouseOver"
@mouseout="handleCanvasObjectMouseOut"
>
<v-rect
v-show="!isUndefined(item.box)"
:config="{
...item.box,
visible:
!isUndefined(item.box) &&
item.isHoverring &&
!isDragging &&
selectedIds.length == 0,
stroke: 'rgb(125,193,239)',
strokeWidth: 2.5,
dash: [10, 5],
cornerRadius: 10,
}"
>
</v-rect>
<v-rect :config="item.config" />
</v-group>
</template>
<v-transformer
ref="transformerRef"
:config="{
borderStroke: 'rgb(125,193,239)',
borderStrokeWidth: 3,
}"
/>
<v-rect
ref="selectRectRef"
v-if="selectionRectangle.visible"
:config="{
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
fill: '#0069FF88',
}"
/>
</v-layer>
</v-stage>
</div>
</template>
<script setup lang="ts">
import Konva from "konva";
import { isNull, isUndefined } from "lodash";
import type {
VGroup,
VLayer,
VNode,
VStage,
VTransformer,
} from "@/utils/VueKonvaType";
import { ref, reactive, watch, onMounted, useTemplateRef } from "vue";
import type { IRect } from "konva/lib/types";
import type { Stage } from "konva/lib/Stage";
const stageSize = {
width: window.innerWidth,
height: window.innerHeight,
};
type CanvasObjectBox = {
x: number;
y: number;
width: number;
height: number;
};
type CanvasObject = {
type: "Rect";
config: Konva.RectConfig;
id: string;
x: number;
y: number;
isHoverring: boolean;
box?: CanvasObjectBox;
};
function calculateRectBounding(
width: number,
height: number,
rotation: number,
padding?: number,
) {
// calculate bounding box for rotated rectangle
const radians = (rotation * Math.PI) / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
// calculate corners of the rectangle
const corners = [
{ x: 0, y: 0 },
{ x: width, y: 0 },
{ x: width, y: height },
{ x: 0, y: height },
].map((point) => ({
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos,
}));
// find bounding box dimensions
const minX = Math.min(...corners.map((p) => p.x));
const maxX = Math.max(...corners.map((p) => p.x));
const minY = Math.min(...corners.map((p) => p.y));
const maxY = Math.max(...corners.map((p) => p.y));
if (padding)
return {
x: minX - padding,
y: minY - padding,
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2,
};
else
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
const objMap = reactive<Map<string, CanvasObject>>(new Map());
onMounted(() => {
for (let n = 0; n < 100; n++) {
const id = Math.round(Math.random() * 10000).toString();
const x = Math.random() * stageSize.width;
const y = Math.random() * stageSize.height;
const width = 30 + Math.random() * 30;
const height = 30 + Math.random() * 30;
const rotation = Math.random() * 180;
objMap.set(id, {
type: "Rect",
config: {
width: width,
height: height,
rotation: rotation,
fill: "grey",
id: id,
},
id: id,
x: x,
y: y,
isHoverring: false,
});
}
});
const layerRef = useTemplateRef<VLayer>("layer");
const canvasObjectsRef = useTemplateRef<HTMLTemplateElement[]>("canvasObjects");
const transformerRef = useTemplateRef<VTransformer>("transformer");
const selectRectRef = useTemplateRef<VNode>("selectRect");
const stageRef = useTemplateRef<VStage>("stage");
const isDragging = ref(false);
const dragItemId = ref<string | null>(null);
const isSelecting = ref(false);
const selectedIds = ref<string[]>([]);
const selectionRectangle = reactive({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
onMounted(() => {
if (isNull(transformerRef.value)) return;
const selectedBox = transformerRef.value.getNode();
selectedBox.resizeEnabled(false);
selectedBox.rotateEnabled(false);
});
function handleCacheChange(e: Event) {
const target = e.target as HTMLInputElement;
const shouldCache = isNull(target) ? false : target.checked;
if (isNull(layerRef.value)) return;
if (shouldCache) {
layerRef.value.getNode().cache();
} else {
layerRef.value.getNode().clearCache();
}
}
// Drag event handlers
function handleDragStart(e: Event) {
isDragging.value = true;
// save drag element:
const target = e.target as unknown as Konva.Node;
dragItemId.value = target.id();
// move current element to the top:
// const item = list.value.find((i) => i.id === dragItemId.value);
// if (isUndefined(item)) return;
//
// const index = list.value.indexOf(item);
// list.value.splice(index, 1);
// list.value.push(item);
}
function handleDragEnd() {
isDragging.value = false;
dragItemId.value = null;
}
// Update transformer nodes when selection changes
watch(selectedIds, () => {
if (isNull(transformerRef.value)) return;
const nodes = selectedIds.value.map((id) => {
if (isNull(layerRef.value)) {
console.error("layer is null");
return null;
}
for (let node of layerRef.value.getNode().children) {
if (node instanceof Konva.Group && node.id() === `group-${id}`)
return node;
}
}) as Konva.Node[];
if (!isUndefined(nodes)) transformerRef.value.getNode().nodes(nodes);
});
// Mouse event handlers
function handleStageClick(e: { target: any; evt: MouseEvent }) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Shape | Konva.Group;
// if we are selecting with rect, do nothing
if (selectionRectangle.visible) {
return;
}
// if click on empty area - remove all selections
if (target === target.getStage()) {
selectedIds.value = [];
return;
}
const clickedId = target.attrs.id;
// do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = selectedIds.value.includes(clickedId);
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
selectedIds.value = [clickedId];
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
selectedIds.value = selectedIds.value.filter((id) => id !== clickedId);
} else if (metaPressed && !isSelected) {
// add the node into selection
selectedIds.value = [...selectedIds.value, clickedId];
}
}
function handleMouseDown(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we mousedown on any shape
if ((e.target as unknown) !== target.getStage()) {
return;
}
// start selection rectangle
isSelecting.value = true;
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.visible = true;
selectionRectangle.x1 = pos.x;
selectionRectangle.y1 = pos.y;
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseMove(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseUp() {
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
isSelecting.value = false;
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible = false;
});
const selBox = {
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
};
let currentSelectedIds = [];
for (let [key, shape] of objMap) {
const shapeConfig = objMap.get(shape.id);
if (isUndefined(shapeConfig)) {
return;
}
if (isUndefined(shapeConfig.box)) {
if (isUndefined(shapeConfig.box)) {
if (
shapeConfig.config.width &&
shapeConfig.config.height &&
shapeConfig.config.rotation
) {
shapeConfig.box = calculateRectBounding(
shapeConfig.config.width,
shapeConfig.config.height,
shapeConfig.config.rotation,
5,
);
} else {
console.error("Could not calculate rect bounding");
return;
}
}
}
if (
Konva.Util.haveIntersection(selBox, {
x: shapeConfig.box.x + shapeConfig.x,
y: shapeConfig.box.y + shapeConfig.y,
width: shapeConfig.box.width,
height: shapeConfig.box.height,
})
)
currentSelectedIds.push(shapeConfig.id);
}
selectedIds.value = currentSelectedIds;
}
function handleWheel(e: { target: any; evt: MouseEvent & { deltaY: number } }) {
e.evt.preventDefault();
const stage = e.target as Stage;
if (stage === null || stage === undefined) {
console.error("Stage is not defined");
return;
}
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
if (pointer === null || pointer === undefined) {
console.error("Pointer position is not defined");
return;
}
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
// how to scale? Zoom in? Or zoom out?
let direction = e.evt.deltaY < 0 ? 1 : -1;
// when we zoom on trackpad, e.evt.ctrlKey is true
// in that case lets revert direction
if (e.evt.ctrlKey) {
direction = -direction;
}
const scaleBy = 1.05;
const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.position(newPos);
}
function handleCanvasObjectMouseOver(evt: Event) {
if (isNull(evt.target)) return;
const target = evt.target;
let object = null;
if (target instanceof Konva.Group) {
if (!target.hasChildren()) return;
object = target.children[0];
} else if (target instanceof Konva.Rect) {
object = target;
} else {
console.trace(`Not Konva class: ${target}`);
return;
}
// Get client rect
const objectConfig = objMap.get(object.id());
if (isUndefined(objectConfig)) {
return;
}
// Get clientBox for first time
if (isUndefined(objectConfig.box)) {
if (
objectConfig.config.width &&
objectConfig.config.height &&
objectConfig.config.rotation
) {
objectConfig.box = calculateRectBounding(
objectConfig.config.width,
objectConfig.config.height,
objectConfig.config.rotation,
5,
);
} else console.error("Could not calculate rect bounding");
}
objectConfig.isHoverring = true;
}
function handleCanvasObjectMouseOut(evt: Event) {
if (isNull(evt.target)) return;
const target = evt.target;
let object = null;
if (target instanceof Konva.Group) {
if (!target.hasChildren()) return;
object = target.children[0];
} else if (target instanceof Konva.Rect) {
object = target;
} else {
console.trace(`Not Konva class: ${target}`);
return;
}
// Get client rect
const objectConfig = objMap.get(object.id());
if (isUndefined(objectConfig)) {
return;
}
objectConfig.isHoverring = false;
}
</script>

View File

@ -14,7 +14,7 @@ interface VGroup extends VueElement {
}
interface VStage extends VueElement {
getStage(): Konva.Stage
getNode(): Konva.Stage
}
interface VTransformer extends VueElement {

View File

@ -1,435 +1,33 @@
<template>
<div class="h-screen w-screen">
<v-stage class="h-full w-full" ref="stage" :config="stageSize" @mousedown="handleMouseDown"
@mousemove="handleMouseMove" @mouseup="handleMouseUp" @click="handleStageClick">
<v-layer ref="layer">
<template ref="canvasObjects" v-for="item in objMap.values()" :key="item.id">
<v-group :config="{
x: item.x,
y: item.y,
draggable: true,
id: `group-${item.id}`,
}" @dragstart="handleDragStart" @dragend="handleDragEnd" @mouseover="handleCanvasObjectMouseOver"
@mouseout="handleCanvasObjectMouseOut">
<v-rect v-show="!isUndefined(item.box)" :config="{
...item.box,
visible: !isUndefined(item.box) && item.isHoverring && !isDragging && selectedIds.length == 0,
stroke: 'rgb(125,193,239)',
strokeWidth: 2.5,
dash: [10, 5],
cornerRadius: 10
}">
</v-rect>
<v-rect :config="item.config" />
</v-group>
</template>
<v-transformer ref="transformer" :config="{
borderStroke: 'rgb(125,193,239)',
borderStrokeWidth: 3,
}" />
<v-rect ref="selectRect" v-if="selectionRectangle.visible" :config="{
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
fill: '#0069FF88',
}" />
</v-layer>
</v-stage>
<div class="absolute top-20 left-10">
<SplitterGroup id="splitter-group" direction="horizontal">
<SplitterPanel
id="splitter-group-panel-canvas"
:default-size="80"
:min-size="30"
class="bg-white border rounded-xl flex items-center justify-center"
>
<LabCanvas></LabCanvas>
</SplitterPanel>
<SplitterResizeHandle id="splitter-group-resize-handle" class="w-2" />
<SplitterPanel
id="splitter-group-panel-properties"
:min-size="20"
class="bg-white border rounded-xl flex items-center justify-center"
>
Panel A
</SplitterPanel>
</SplitterGroup>
<!-- <div class="absolute top-20 left-10">
<input type="checkbox" class="checkbox" @change="handleCacheChange" />
cache shapes
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { isNull, isUndefined } from "mathjs";
import Konva from "konva";
import type {
VGroup,
VLayer,
VNode,
VStage,
VTransformer,
} from "@/utils/VueKonvaType";
import { ref, reactive, watch, onMounted, useTemplateRef } from "vue";
import { list } from "postcss";
import type { IRect } from "konva/lib/types";
const stageSize = {
width: window.innerWidth,
height: window.innerHeight,
};
type CanvasObjectBox = {
x: number;
y: number;
width: number;
height: number;
};
type CanvasObject = {
type: "Rect";
config: Konva.RectConfig;
id: string;
x: number;
y: number;
isHoverring: boolean;
box?: CanvasObjectBox;
};
function calculateRectBounding(
width: number,
height: number,
rotation: number,
padding?: number,
) {
// calculate bounding box for rotated rectangle
const radians = (rotation * Math.PI) / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
// calculate corners of the rectangle
const corners = [
{ x: 0, y: 0 },
{ x: width, y: 0 },
{ x: width, y: height },
{ x: 0, y: height },
].map((point) => ({
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos,
}));
// find bounding box dimensions
const minX = Math.min(...corners.map((p) => p.x));
const maxX = Math.max(...corners.map((p) => p.x));
const minY = Math.min(...corners.map((p) => p.y));
const maxY = Math.max(...corners.map((p) => p.y));
if (padding)
return {
x: minX - padding,
y: minY - padding,
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2,
};
else
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
const objMap = reactive<Map<string, CanvasObject>>(new Map());
onMounted(() => {
for (let n = 0; n < 100; n++) {
const id = Math.round(Math.random() * 10000).toString();
const x = Math.random() * stageSize.width;
const y = Math.random() * stageSize.height;
const width = 30 + Math.random() * 30;
const height = 30 + Math.random() * 30;
const rotation = Math.random() * 180;
objMap.set(id, {
type: "Rect",
config: {
width: width,
height: height,
rotation: rotation,
fill: "grey",
id: id,
},
id: id,
x: x,
y: y,
isHoverring: false,
});
}
});
const layer = useTemplateRef<VLayer>("layer");
const canvasObjects = useTemplateRef<HTMLTemplateElement[]>("canvasObjects")
const transformer = useTemplateRef<VTransformer>("transformer");
const selectRect = useTemplateRef<VNode>("selectRect");
const stage = useTemplateRef<VStage>("stage");
const isDragging = ref(false);
const dragItemId = ref<string | null>(null);
const isSelecting = ref(false);
const selectedIds = ref<string[]>([]);
const selectionRectangle = reactive({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
onMounted(() => {
if (isNull(transformer.value)) return;
const selectedBox = transformer.value.getNode();
selectedBox.resizeEnabled(false);
selectedBox.rotateEnabled(false);
});
function handleCacheChange(e: Event) {
const target = e.target as HTMLInputElement;
const shouldCache = isNull(target) ? false : target.checked;
if (isNull(layer.value)) return;
if (shouldCache) {
layer.value.getNode().cache();
} else {
layer.value.getNode().clearCache();
}
}
// Drag event handlers
function handleDragStart(e: Event) {
isDragging.value = true;
// save drag element:
const target = e.target as unknown as Konva.Node;
dragItemId.value = target.id();
// move current element to the top:
// const item = list.value.find((i) => i.id === dragItemId.value);
// if (isUndefined(item)) return;
//
// const index = list.value.indexOf(item);
// list.value.splice(index, 1);
// list.value.push(item);
}
function handleDragEnd() {
isDragging.value = false;
dragItemId.value = null;
}
// Update transformer nodes when selection changes
watch(selectedIds, () => {
if (isNull(transformer.value)) return;
const nodes = selectedIds.value
.map((id) => {
if (isNull(layer.value)) {
console.error("layer is null")
return null;
}
for (let node of layer.value.getNode().children) {
if (
node instanceof Konva.Group &&
node.id() === `group-${id}`
) return node;
}
}) as Konva.Node[]
if (!isUndefined(nodes)) transformer.value.getNode().nodes(nodes);
});
// Mouse event handlers
function handleStageClick(e: { target: any, evt: MouseEvent }) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Shape | Konva.Group;
// if we are selecting with rect, do nothing
if (selectionRectangle.visible) {
return;
}
// if click on empty area - remove all selections
if (target === target.getStage()) {
selectedIds.value = [];
return;
}
const clickedId = target.attrs.id;
// do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = selectedIds.value.includes(clickedId);
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
selectedIds.value = [clickedId];
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
selectedIds.value = selectedIds.value.filter(id => id !== clickedId);
} else if (metaPressed && !isSelected) {
// add the node into selection
selectedIds.value = [...selectedIds.value, clickedId];
}
}
function handleMouseDown(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we mousedown on any shape
if ((e.target as unknown) !== target.getStage()) {
return;
}
// start selection rectangle
isSelecting.value = true;
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.visible = true;
selectionRectangle.x1 = pos.x;
selectionRectangle.y1 = pos.y;
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseMove(e: Event) {
if (isNull(e.target)) return;
const target = e.target as unknown as Konva.Container;
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
const pos = target.getStage()?.getPointerPosition();
if (!isNull(pos) && !isUndefined(pos)) {
selectionRectangle.x2 = pos.x;
selectionRectangle.y2 = pos.y;
}
}
function handleMouseUp() {
// do nothing if we didn't start selection
if (!isSelecting.value) {
return;
}
isSelecting.value = false;
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible = false;
});
const selBox = {
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
};
let currentSelectedIds = [];
for (let [key, shape] of objMap) {
const shapeConfig = objMap.get(shape.id);
if (isUndefined(shapeConfig)) {
return;
}
if (isUndefined(shapeConfig.box)) {
if (isUndefined(shapeConfig.box)) {
if (
shapeConfig.config.width &&
shapeConfig.config.height &&
shapeConfig.config.rotation
) {
shapeConfig.box = calculateRectBounding(
shapeConfig.config.width,
shapeConfig.config.height,
shapeConfig.config.rotation,
5,
);
} else {
console.error("Could not calculate rect bounding");
return;
}
}
}
if (Konva.Util.haveIntersection(selBox, {
x: shapeConfig.box.x + shapeConfig.x,
y: shapeConfig.box.y + shapeConfig.y,
width: shapeConfig.box.width,
height: shapeConfig.box.height,
}))
currentSelectedIds.push(shapeConfig.id)
}
selectedIds.value = currentSelectedIds;
}
function handleCanvasObjectMouseOver(evt: Event) {
if (isNull(evt.target)) return;
const target = evt.target;
let object = null;
if (target instanceof Konva.Group) {
if (!target.hasChildren()) return;
object = target.children[0];
} else if (target instanceof Konva.Rect) {
object = target;
} else {
console.trace(`Not Konva class: ${target}`);
return;
}
// Get client rect
const objectConfig = objMap.get(object.id());
if (isUndefined(objectConfig)) {
return;
}
// Get clientBox for first time
if (isUndefined(objectConfig.box)) {
if (
objectConfig.config.width &&
objectConfig.config.height &&
objectConfig.config.rotation
) {
objectConfig.box = calculateRectBounding(
objectConfig.config.width,
objectConfig.config.height,
objectConfig.config.rotation,
5,
);
} else console.error("Could not calculate rect bounding");
}
objectConfig.isHoverring = true;
}
function handleCanvasObjectMouseOut(evt: Event) {
if (isNull(evt.target)) return;
const target = evt.target;
let object = null;
if (target instanceof Konva.Group) {
if (!target.hasChildren()) return;
object = target.children[0];
} else if (target instanceof Konva.Rect) {
object = target;
} else {
console.trace(`Not Konva class: ${target}`);
return;
}
// Get client rect
const objectConfig = objMap.get(object.id());
if (isUndefined(objectConfig)) {
return;
}
objectConfig.isHoverring = false;
}
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
import LabCanvas from "@/components/LabCanvas.vue";
</script>
<style>
@ -440,6 +38,6 @@ function handleCanvasObjectMouseOut(evt: Event) {
@import "../assets/main.css";
.primary {
color: rgb(125, 193, 239)
color: rgb(125, 193, 239);
}
</style>
</style>

View File

@ -6,6 +6,8 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
import Components from 'unplugin-vue-components/vite'
import RekaResolver from 'reka-ui/resolver'
// https://vite.dev/config/
export default defineConfig({
@ -20,6 +22,18 @@ export default defineConfig({
}),
vueJsx(),
vueDevTools(),
Components(
{
dts: true,
resolvers: [
RekaResolver()
// RekaResolver({
// prefix: '' // use the prefix option to add Prefix to the imported components
// })
],
}
)
],
resolve: {
alias: {