feat: add resizer and add zoom in / out for canvas
This commit is contained in:
		
							
								
								
									
										50
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
										Normal 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
									
									
									
								
							
							
						
						
									
										615
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										10
									
								
								src/assets/base.css
									
									
									
									
									
										Normal 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] *));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										499
									
								
								src/components/LabCanvas.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								src/components/LabCanvas.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@@ -14,7 +14,7 @@ interface VGroup extends VueElement {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VStage extends VueElement {
 | 
			
		||||
  getStage(): Konva.Stage
 | 
			
		||||
  getNode(): Konva.Stage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VTransformer extends VueElement {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user