diff --git a/src/main/ts/index.html b/src/main/ts/index.html
new file mode 100644
index 000000000..bc4f23150
--- /dev/null
+++ b/src/main/ts/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Vite + TS
+
+
+
+ 拖拽文件进入此处
+
+
+
+
+
+
+
diff --git a/src/main/ts/package.json b/src/main/ts/package.json
new file mode 100644
index 000000000..54e59979f
--- /dev/null
+++ b/src/main/ts/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "test-ts3",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@types/js-yaml": "^4.0.5",
+ "typescript": "^5.0.2",
+ "vite": "^4.3.2"
+ },
+ "dependencies": {
+ "gojs": "^2.3.7",
+ "js-yaml": "^4.1.0",
+ "yaml": "^2.3.0-5"
+ }
+}
diff --git a/src/main/ts/pnpm-lock.yaml b/src/main/ts/pnpm-lock.yaml
new file mode 100644
index 000000000..fa8a86b2c
--- /dev/null
+++ b/src/main/ts/pnpm-lock.yaml
@@ -0,0 +1,427 @@
+lockfileVersion: '6.0'
+
+dependencies:
+ gojs:
+ specifier: ^2.3.7
+ version: registry.npmmirror.com/gojs@2.3.7
+ js-yaml:
+ specifier: ^4.1.0
+ version: registry.npmmirror.com/js-yaml@4.1.0
+ yaml:
+ specifier: ^2.3.0-5
+ version: registry.npmmirror.com/yaml@2.3.0-5
+
+devDependencies:
+ '@types/js-yaml':
+ specifier: ^4.0.5
+ version: registry.npmmirror.com/@types/js-yaml@4.0.5
+ typescript:
+ specifier: ^5.0.2
+ version: registry.npmmirror.com/typescript@5.0.2
+ vite:
+ specifier: ^4.3.2
+ version: registry.npmmirror.com/vite@4.3.2
+
+packages:
+
+ registry.npmmirror.com/@esbuild/android-arm64@0.17.19:
+ resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz}
+ name: '@esbuild/android-arm64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/android-arm@0.17.19:
+ resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz}
+ name: '@esbuild/android-arm'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/android-x64@0.17.19:
+ resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz}
+ name: '@esbuild/android-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/darwin-arm64@0.17.19:
+ resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz}
+ name: '@esbuild/darwin-arm64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/darwin-x64@0.17.19:
+ resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz}
+ name: '@esbuild/darwin-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/freebsd-arm64@0.17.19:
+ resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz}
+ name: '@esbuild/freebsd-arm64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/freebsd-x64@0.17.19:
+ resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz}
+ name: '@esbuild/freebsd-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-arm64@0.17.19:
+ resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz}
+ name: '@esbuild/linux-arm64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-arm@0.17.19:
+ resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz}
+ name: '@esbuild/linux-arm'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-ia32@0.17.19:
+ resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz}
+ name: '@esbuild/linux-ia32'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-loong64@0.17.19:
+ resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz}
+ name: '@esbuild/linux-loong64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-mips64el@0.17.19:
+ resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz}
+ name: '@esbuild/linux-mips64el'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-ppc64@0.17.19:
+ resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz}
+ name: '@esbuild/linux-ppc64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-riscv64@0.17.19:
+ resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz}
+ name: '@esbuild/linux-riscv64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-s390x@0.17.19:
+ resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz}
+ name: '@esbuild/linux-s390x'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/linux-x64@0.17.19:
+ resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz}
+ name: '@esbuild/linux-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/netbsd-x64@0.17.19:
+ resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz}
+ name: '@esbuild/netbsd-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/openbsd-x64@0.17.19:
+ resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz}
+ name: '@esbuild/openbsd-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/sunos-x64@0.17.19:
+ resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz}
+ name: '@esbuild/sunos-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/win32-arm64@0.17.19:
+ resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz}
+ name: '@esbuild/win32-arm64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/win32-ia32@0.17.19:
+ resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz}
+ name: '@esbuild/win32-ia32'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@esbuild/win32-x64@0.17.19:
+ resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz}
+ name: '@esbuild/win32-x64'
+ version: 0.17.19
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/@types/js-yaml@4.0.5:
+ resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.5.tgz}
+ name: '@types/js-yaml'
+ version: 4.0.5
+ dev: true
+
+ registry.npmmirror.com/argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz}
+ name: argparse
+ version: 2.0.1
+ dev: false
+
+ registry.npmmirror.com/esbuild@0.17.19:
+ resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz}
+ name: esbuild
+ version: 0.17.19
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': registry.npmmirror.com/@esbuild/android-arm@0.17.19
+ '@esbuild/android-arm64': registry.npmmirror.com/@esbuild/android-arm64@0.17.19
+ '@esbuild/android-x64': registry.npmmirror.com/@esbuild/android-x64@0.17.19
+ '@esbuild/darwin-arm64': registry.npmmirror.com/@esbuild/darwin-arm64@0.17.19
+ '@esbuild/darwin-x64': registry.npmmirror.com/@esbuild/darwin-x64@0.17.19
+ '@esbuild/freebsd-arm64': registry.npmmirror.com/@esbuild/freebsd-arm64@0.17.19
+ '@esbuild/freebsd-x64': registry.npmmirror.com/@esbuild/freebsd-x64@0.17.19
+ '@esbuild/linux-arm': registry.npmmirror.com/@esbuild/linux-arm@0.17.19
+ '@esbuild/linux-arm64': registry.npmmirror.com/@esbuild/linux-arm64@0.17.19
+ '@esbuild/linux-ia32': registry.npmmirror.com/@esbuild/linux-ia32@0.17.19
+ '@esbuild/linux-loong64': registry.npmmirror.com/@esbuild/linux-loong64@0.17.19
+ '@esbuild/linux-mips64el': registry.npmmirror.com/@esbuild/linux-mips64el@0.17.19
+ '@esbuild/linux-ppc64': registry.npmmirror.com/@esbuild/linux-ppc64@0.17.19
+ '@esbuild/linux-riscv64': registry.npmmirror.com/@esbuild/linux-riscv64@0.17.19
+ '@esbuild/linux-s390x': registry.npmmirror.com/@esbuild/linux-s390x@0.17.19
+ '@esbuild/linux-x64': registry.npmmirror.com/@esbuild/linux-x64@0.17.19
+ '@esbuild/netbsd-x64': registry.npmmirror.com/@esbuild/netbsd-x64@0.17.19
+ '@esbuild/openbsd-x64': registry.npmmirror.com/@esbuild/openbsd-x64@0.17.19
+ '@esbuild/sunos-x64': registry.npmmirror.com/@esbuild/sunos-x64@0.17.19
+ '@esbuild/win32-arm64': registry.npmmirror.com/@esbuild/win32-arm64@0.17.19
+ '@esbuild/win32-ia32': registry.npmmirror.com/@esbuild/win32-ia32@0.17.19
+ '@esbuild/win32-x64': registry.npmmirror.com/@esbuild/win32-x64@0.17.19
+ dev: true
+
+ registry.npmmirror.com/fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz}
+ name: fsevents
+ version: 2.3.2
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ registry.npmmirror.com/gojs@2.3.7:
+ resolution: {integrity: sha512-UX/2G8IeU4GErAo3ZCtkSpTJ6xvIqr5TOoJGjPLJ4DRukDweVk78scsE99wohZC2oqDOIrKEathUmHiskRGong==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gojs/-/gojs-2.3.7.tgz}
+ name: gojs
+ version: 2.3.7
+ dev: false
+
+ registry.npmmirror.com/js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz}
+ name: js-yaml
+ version: 4.1.0
+ hasBin: true
+ dependencies:
+ argparse: registry.npmmirror.com/argparse@2.0.1
+ dev: false
+
+ registry.npmmirror.com/nanoid@3.3.6:
+ resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz}
+ name: nanoid
+ version: 3.3.6
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ registry.npmmirror.com/picocolors@1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz}
+ name: picocolors
+ version: 1.0.0
+ dev: true
+
+ registry.npmmirror.com/postcss@8.4.23:
+ resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz}
+ name: postcss
+ version: 8.4.23
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: registry.npmmirror.com/nanoid@3.3.6
+ picocolors: registry.npmmirror.com/picocolors@1.0.0
+ source-map-js: registry.npmmirror.com/source-map-js@1.0.2
+ dev: true
+
+ registry.npmmirror.com/rollup@3.22.0:
+ resolution: {integrity: sha512-imsigcWor5Y/dC0rz2q0bBt9PabcL3TORry2hAa6O6BuMvY71bqHyfReAz5qyAqiQATD1m70qdntqBfBQjVWpQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/rollup/-/rollup-3.22.0.tgz}
+ name: rollup
+ version: 3.22.0
+ engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: registry.npmmirror.com/fsevents@2.3.2
+ dev: true
+
+ registry.npmmirror.com/source-map-js@1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz}
+ name: source-map-js
+ version: 1.0.2
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ registry.npmmirror.com/typescript@5.0.2:
+ resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/typescript/-/typescript-5.0.2.tgz}
+ name: typescript
+ version: 5.0.2
+ engines: {node: '>=12.20'}
+ hasBin: true
+ dev: true
+
+ registry.npmmirror.com/vite@4.3.2:
+ resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vite/-/vite-4.3.2.tgz}
+ name: vite
+ version: 4.3.2
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': '>= 14'
+ less: '*'
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: registry.npmmirror.com/esbuild@0.17.19
+ postcss: registry.npmmirror.com/postcss@8.4.23
+ rollup: registry.npmmirror.com/rollup@3.22.0
+ optionalDependencies:
+ fsevents: registry.npmmirror.com/fsevents@2.3.2
+ dev: true
+
+ registry.npmmirror.com/yaml@2.3.0-5:
+ resolution: {integrity: sha512-Y2NEcp/A4OjsMxDenJrPqucy/ufkbfQu7RRjKYWLfyATH+oGJKkPWp/4mnHPCnBpIUH29P/F8uemt/g0xB3VQg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/yaml/-/yaml-2.3.0-5.tgz}
+ name: yaml
+ version: 2.3.0-5
+ engines: {node: '>= 14', npm: '>= 7'}
+ dev: false
diff --git a/src/main/ts/src/graph.ts b/src/main/ts/src/graph.ts
new file mode 100644
index 000000000..c64bb39a5
--- /dev/null
+++ b/src/main/ts/src/graph.ts
@@ -0,0 +1,152 @@
+import { parse } from "yaml";
+
+export class Graph{
+ metadata: Metadata;
+ relation: Relation;
+ graph: Map;
+ sourceNodes: number[];
+ sinkNodes: number[];
+ recommendedPaths: number[][];
+
+ constructor(data: any){
+ this.metadata = new Metadata(data.metadata);
+ this.relation = new Relation(data.relation, this.metadata);
+ this.graph = new Map();
+ this.initGraph(data.graph);
+ this.sourceNodes = data.sourceNodes;
+ this.sinkNodes = data.sinkNodes;
+
+ this.recommendedPaths = (data.recommendedPaths as number[][]).map(path=>path.filter(n => !this.isField(n)));
+ // console.log(this.recommandedPaths);
+ // console.log(this.sourceNodes + " " + this.sinkNodes)
+ // console.log(this.metadata.getvf(6))
+ // console.log(this.metadata.getvf(25))
+ // console.log(this.metadata.getvf(26))
+ // console.log(this.metadata.getvf(27))
+ }
+
+ initGraph(graph: any){
+ Object.entries(graph).forEach(entry=>{
+ const toNodess: number[] = [];
+ (entry[1] as number[]).forEach(n=>{
+ if(this.isField(n)){
+ toNodess.push(...(graph[n] as number[]));
+ }else{
+ toNodess.push(n);
+ }
+ });
+ const toNodes = Array.from(new Set(toNodess));
+ this.graph.set(parseInt(entry[0]), toNodes);
+ });
+ }
+
+ isField(num:number){
+ return this.relation.fields.includes(num);
+ }
+
+ isSource(num:number){
+ return this.sourceNodes.includes(num);
+ }
+
+ isSink(num:number){
+ return this.sinkNodes.includes(num);
+ }
+}
+
+class Metadata{
+ packages: string[];
+ classes: string[];
+ methods: string[];
+ varsAndFields: string[];
+
+ constructor(metadata: any){
+ this.packages = metadata.packages;
+ this.classes = metadata.classes;
+ this.methods = metadata.methods;
+ this.varsAndFields = metadata.varsAndFields;
+
+ const len: number = this.classes.length;
+ for(let i = 0; i < len; i++){
+ if(!this.classes[i].includes('.')){
+ // set the class with no parent package different to its package
+ this.classes[i] = '.' + this.classes[i];
+ }
+ }
+ }
+
+ getp(index: number) {
+ return this.packages[index];
+ }
+
+ getc(index: number) {
+ return this.classes[index];
+ }
+
+ getm(index: number) {
+ return this.methods[index];
+ }
+
+ getvf(index: number) {
+ return this.varsAndFields[index];
+ }
+}
+
+class Relation{
+ packageToClasses: Map;
+ classToMethods: Map;
+ methodToVars: Map;
+
+ fields: number[];
+ // jdkVars: number[];
+
+ constructor(relation: any, metadata: Metadata){
+ this.packageToClasses = new Map();
+ this.classToMethods = new Map();
+ this.methodToVars = new Map();
+ this.fields = Object.values(relation.classToFields).flat() as number[];
+
+ // const jdkPackages: number[] = [];
+ Object.entries(relation.packageToClasses).forEach(entry=>{
+ this.packageToClasses.set(parseInt(entry[0]), entry[1] as number[]);
+ // if(metadata.getp(parseInt(entry[0])) === "java.lang"){ // todo:
+ // jdkPackages.push(parseInt(entry[0]));
+ // }
+ });
+
+ Object.entries(relation.classToMethods).forEach(entry=>{
+ this.classToMethods.set(parseInt(entry[0]), entry[1] as number[]);
+ });
+
+ Object.entries(relation.methodToVars).forEach(entry=>{
+ this.methodToVars.set(parseInt(entry[0]), entry[1] as number[]);
+ });
+
+ // const jdkClasses: number[] = (Array.from(jdkPackages, p => this.packageToClasses.get(p)).flat().filter(item => item !== undefined) as number[]);
+ // const jdkMethods: number[] = (Array.from(jdkClasses, p => this.classToMethods.get(p)).flat().filter(item => item !== undefined) as number[]);
+ // this.jdkVars = (Array.from(jdkMethods, p => this.methodToVars.get(p)).flat().filter(item => item !== undefined) as number[]);
+ }
+
+}
+
+export function build(file:File, visualize:(graph:Graph)=>void){
+ const reader = new FileReader();
+
+ reader.onload = function(e){
+ const yamlContent = e.target?.result;
+ if(typeof yamlContent != 'string'){
+ console.log('yamlContent不是字符串类型');
+ return;
+ }
+ // try{
+ const parsedData = parse((yamlContent as string));
+ const graph:Graph = new Graph(parsedData);
+ console.log('finish build, begin visualization');
+ visualize(graph);
+ // }
+ // catch{
+ // console.log(e);
+ // }
+ }
+
+ reader.readAsText(file);
+}
\ No newline at end of file
diff --git a/src/main/ts/src/handle-event.ts b/src/main/ts/src/handle-event.ts
new file mode 100644
index 000000000..92c8ac949
--- /dev/null
+++ b/src/main/ts/src/handle-event.ts
@@ -0,0 +1,33 @@
+export function eventHandler(elem: HTMLElement, fileHandler: (file: File)=>void){
+ elem.addEventListener('dragenter', function(event) {
+ event.preventDefault();
+ elem.style.background = '#f7f7f7';
+ elem.textContent = "松开文件进行处理";
+ });
+ elem.addEventListener('dragleave', function(event) {
+ event.preventDefault();
+ elem.style.background = '#ffffff';
+ elem.textContent = "拖拽文件进入此处";
+ });
+ elem.addEventListener('dragover', function(event) {
+ event.preventDefault();
+ });
+ elem.addEventListener('drop', function(event){
+ event.preventDefault();
+ elem.style.background = '#ffffff';
+
+ let file = (event as DragEvent).dataTransfer?.files[0];
+
+ if(file === undefined){
+ elem.textContent = "未接收到文件";
+ return;
+ }
+ if(!file.name.endsWith('.yml')){
+ elem.textContent = "接收到非yaml文件";
+ return;
+ }
+ elem.textContent = "拖拽文件进入此处";
+ console.log('handle event over, begin building graph');
+ fileHandler(file);
+ });
+}
\ No newline at end of file
diff --git a/src/main/ts/src/main.ts b/src/main/ts/src/main.ts
new file mode 100644
index 000000000..200c75691
--- /dev/null
+++ b/src/main/ts/src/main.ts
@@ -0,0 +1,10 @@
+import './style.css'
+import { build } from './graph'
+import { eventHandler } from './handle-event'
+import { visualize } from './visualization';
+
+const inputElem: HTMLElement = document.getElementById('input') as HTMLElement;
+eventHandler(inputElem, (file: File)=>{
+ build(file, visualize);
+});
+
diff --git a/src/main/ts/src/style.css b/src/main/ts/src/style.css
new file mode 100644
index 000000000..05039e094
--- /dev/null
+++ b/src/main/ts/src/style.css
@@ -0,0 +1,106 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #3178c6aa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.button-container {
+ position: absolute; /* 设置容器为绝对定位 */
+ top: 40px; /* 距离页面顶部 40px */
+ left: 20px; /* 距离页面左侧 20px */
+ display: flex; /* 使用 flexbox 布局 */
+ flex-direction: column; /* 设置为垂直排列 */
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid #1a1a1a;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+ margin-bottom: 10px;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/src/main/ts/src/visualization.ts b/src/main/ts/src/visualization.ts
new file mode 100644
index 000000000..18c10c3d7
--- /dev/null
+++ b/src/main/ts/src/visualization.ts
@@ -0,0 +1,551 @@
+import go, { Group, Model } from 'gojs';
+import { Graph } from './graph' ;
+import { Alias } from 'yaml';
+
+// const $ = go.GraphObject.make;
+// const myDiagram = $(go.Diagram, "myDiagramDiv",
+// {
+// // layout: $(go.GridLayout,
+// // {
+// // wrappingColumn: 6,
+// // arrangement: go.GridLayout.Ascending,
+// // spacing: new go.Size(50,50),
+// // isRealtime: false,
+// // })
+// layout: $(go.TreeLayout,
+// {angle: 90}
+// ),
+// "undoManager.isEnabled": true
+// });
+
+const $ = go.GraphObject.make;
+
+const myDiagram =
+new go.Diagram("myDiagramDiv",
+ {
+ padding: 10,
+ layout: $(go.LayeredDigraphLayout,
+ {
+ direction: 90,
+ layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
+ alignOption: go.LayeredDigraphLayout.AlignAll
+ }),
+ "undoManager.isEnabled": true
+ });
+
+export function visualize(graph: Graph){
+ const nodeDataArray = makeNodes(graph);
+ const linkDataArray = makeLinks(graph);
+
+ myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
+
+ myDiagram.nodeTemplate =
+ $(go.Node, "Auto",
+ new go.Binding("visible"),
+ $(go.Shape, "Rectangle", {fill: "lightblue"},
+ new go.Binding("fill", "color"),
+ new go.Binding("stroke", "isHighlighted", h => h ? "red" : "black").ofObject(),
+ new go.Binding("strokeWidth", "isHighlighted", h => h ? 4 : 2).ofObject(),
+ ),
+ $(go.TextBlock, "isla", { margin: 10 },
+ new go.Binding("text", "key")
+ ),
+ {
+ selectionAdornmentTemplate:
+ $(go.Adornment, "Spot",
+ $(go.Panel, "Auto",
+ $(go.Shape, { fill: null, stroke: "black", strokeWidth: 4 }),
+ $(go.Placeholder),
+ ),
+ $("Button",
+ $(go.TextBlock, "trace"),
+ { alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Center, desiredSize: new go.Size(70,25), click: highlightPath },
+ )
+ ),
+ },
+ $("Button", // a replacement for "TreeExpanderButton" that works for non-tree-structured graphs
+ // assume initially not visible because there are no links coming out
+ { visible: false },
+ // bind the button visibility to whether it's not a leaf node
+ new go.Binding("visible", "isTreeLeaf", leaf => !leaf).ofObject(),
+ $(go.Shape,
+ {
+ name: "ButtonIcon",
+ figure: "MinusLine",
+ desiredSize: new go.Size(6, 6)
+ },
+ new go.Binding("figure", "isCollapsed", // data.isCollapsed remembers "collapsed" or "expanded"
+ collapsed => collapsed ? "PlusLine" : "MinusLine")),
+ {
+ click: (e, obj) => {
+ e.diagram.startTransaction();
+ var node = obj.part as go.Node;
+ if (node.data.isCollapsed) {
+ expandFrom(node, node);
+ } else {
+ collapseFrom(node, node);
+ }
+ e.diagram.commitTransaction("toggled visibility of dependencies");
+ }
+ }
+ )
+ );
+
+ myDiagram.linkTemplate =
+ $(go.Link,
+ { curve: go.Link.Bezier },
+ $(go.Shape, { strokeWidth: 3 },
+ new go.Binding("stroke", "isHighlighted", h => h ? "red" : "black").ofObject(),
+ new go.Binding("strokeWidth", "isHighlighted", h => h ? 4 : 2).ofObject(),
+ ),
+ $(go.Shape, { toArrow: "Standard", strokeWidth: 3 },
+ new go.Binding("stroke", "isHighlighted", h => h ? "red" : "black").ofObject(),
+ ),
+ );
+
+ myDiagram.groupTemplate =
+ $(go.Group, "Auto",
+ {
+ // layout: $(go.GridLayout,
+ // { wrappingColumn: 2, arrangement: go.GridLayout.Ascending, spacing: new go.Size(50,50), isRealtime: false }),
+ // isSubGraphExpanded: false,
+ layout: $(go.LayeredDigraphLayout,
+ {
+ direction: 90,
+ layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
+ alignOption: go.LayeredDigraphLayout.AlignAll
+ }),
+ isSubGraphExpanded: false
+ },
+ $(go.Shape, "RoundedRectangle",
+ { fill: null, stroke: "gray", strokeWidth: 2 }),
+ $(go.Panel, "Vertical",
+ { defaultAlignment: go.Spot.Left, margin: 4 },
+ $(go.Panel, "Horizontal",
+ { defaultAlignment: go.Spot.Top },
+ $("SubGraphExpanderButton"),
+ $(go.TextBlock,
+ { font: "Bold 18px Sans-Serif", margin: 4 },
+ new go.Binding("text", "key")),
+ ),
+ $(go.Placeholder,
+ { padding: new go.Margin(0, 10) }),
+ ),
+ );
+ myDiagram.nodes.each(function(n) {
+ // myDiagram.startTransaction();
+ n.visible = false;
+ // myDiagram.commitTransaction("hide all nodes");
+ })
+ myDiagram.nodes.each(function(n) {
+ if(n instanceof go.Node && !(n instanceof go.Group) && !n.findLinksInto().next()){
+ var nd = n as go.Node;
+ nd.visible = true;
+ while(nd.containingGroup !== null){
+ (nd.containingGroup as go.Group).visible = true;
+ nd = nd.containingGroup;
+ }
+ }
+ })
+ myDiagram.links.each(link => link.visible = false);
+
+ if(!buttonCreated){
+ LayersChangeButton("Remove Package", "Restore Package", graph, restorePackages, removePackages);
+ LayersChangeButton("Remove Classes", "Restore Class", graph, restoreClasses, removeClasses);
+ LayersChangeButton("Remove Methods", "Restore Method", graph, restoreMethods, removeMethods);
+
+ HighlightPathButton("Highlight Recommanded Paths", graph, highlightRecommandedPath);
+
+ ResetButton(graph, hideAll);
+ buttonCreated = true;
+ }
+
+ // myDiagram.layout = $(go.TreeLayout, { angle: 90 });
+
+ // myDiagram.model = new go.GraphLinksModel(
+ // [ { key: 1 },
+ // { key: 2 },
+ // { key: 3 },
+ // ],
+ // [ { from: 1, to: 3 },
+ // { from: 2, to: 3 }] );
+ // myDiagram.nodes.each(function(n) {
+ // n.wasTreeExpanded = false;
+ // n.isTreeExpanded = false;
+ // })
+}
+
+// ===================== LayersChangeButton Function =========================
+
+var buttonCreated = false;
+// 记录各层次是否还在图中出现,如packageExist = false代表图中不出现Package层次
+let packageExist = true;
+let classExist = true;
+let methodExist = true;
+function LayersChangeButton(restoreState : string, removeState : string, graph : Graph, restoreFunc : (graph : Graph) => void, removeFunc : (graph : Graph) => void){
+ var button = document.createElement("button");
+ button.innerHTML = restoreState;
+ document.getElementById("buttonContainer")?.appendChild(button);
+ button.addEventListener("click", function(){
+ if(button.innerHTML === restoreState){
+ button.innerHTML = removeState;
+ removeFunc(graph);
+ }
+ else if(button.innerHTML === removeState){
+ button.innerHTML = restoreState;
+ restoreFunc(graph);
+ }
+ })
+}
+
+function removePackages(graph : Graph){
+ myDiagram.startTransaction("remove Package");
+ graph.metadata.packages.forEach(removeGroup);
+ myDiagram.commitTransaction("remove Package");
+ packageExist = false;
+}
+
+function removeClasses(graph : Graph){
+ myDiagram.startTransaction("remove Class");
+ graph.metadata.classes.forEach(removeGroup);
+ myDiagram.commitTransaction("remove Class");
+ classExist = false;
+}
+
+function removeMethods(graph : Graph){
+ myDiagram.startTransaction("remove Method");
+ graph.metadata.methods.forEach(removeGroup);
+ myDiagram.commitTransaction("remove Method");
+ methodExist = false;
+}
+
+function removeGroup(name: string){
+ var group = myDiagram.findNodeForKey(name) as go.Group;
+ if(group === null) return;
+
+ (group.diagram as go.Diagram).model.setDataProperty(group, "isSubGraphExpanded", true);
+ group.memberParts.each(nodeOrGroup => (nodeOrGroup.diagram as go.Diagram).model.setDataProperty(nodeOrGroup.data, "group", group.containingGroup? group.containingGroup.key : null));
+ myDiagram.model.removeNodeData(myDiagram.model.findNodeDataForKey(name) as go.ObjectData);
+}
+
+
+function restorePackages(graph : Graph){
+ myDiagram.startTransaction("restore Package");
+ // 恢复Package层需要先恢复Class层,而后再将Class层删去,做到只恢复Package层的效果
+ if(!classExist){
+ restoreClasses(graph);
+ graph.relation.packageToClasses.forEach((vc,kp) => restoreGroup(graph.metadata.getp(kp), vc.filter(v => v !== undefined).map(v => graph.metadata.getc(v))));
+ removeClasses(graph);
+ }else{
+ graph.relation.packageToClasses.forEach((vc,kp) => restoreGroup(graph.metadata.getp(kp), vc.filter(v => v !== undefined).map(v => graph.metadata.getc(v))));
+ }
+ myDiagram.commitTransaction("restore Package");
+ packageExist = true;
+}
+
+function restoreClasses(graph : Graph){
+ myDiagram.startTransaction("restore Class");
+ // 恢复Class层需要先恢复Method层,理由同上
+ if(!methodExist){
+ restoreMethods(graph);
+ graph.relation.classToMethods.forEach((vm,kc) => restoreGroup(graph.metadata.getc(kc), vm.filter(v => v !== undefined).map(v => graph.metadata.getm(v))));
+ removeMethods(graph);
+ }else{
+ graph.relation.classToMethods.forEach((vm,kc) => restoreGroup(graph.metadata.getc(kc), vm.filter(v => v !== undefined).map(v => graph.metadata.getm(v))));
+ }
+ myDiagram.commitTransaction("restore Class");
+ classExist = true;
+}
+
+function restoreMethods(graph : Graph){
+ myDiagram.startTransaction("restore Method");
+ graph.relation.methodToVars.forEach((vv,km) =>
+ restoreGroup(graph.metadata.getm(km), vv.filter(v => v !== undefined).map(v => graph.metadata.getvf(v))));
+ myDiagram.commitTransaction("restore Method");
+ methodExist = true;
+}
+
+function restoreGroup(name: string, members : string[]){
+ myDiagram.model.addNodeData({key: name, isGroup: true, isHighlighted: false, isCollapsed: false});
+
+ const parent = myDiagram.findNodeForKey(name) as go.Group; // 要恢复的Group
+ let containGroupKey;
+
+ // 将要恢复的Group展开
+ (parent.diagram as go.Diagram).model.setDataProperty(parent, "isSubGraphExpanded", true);
+ members.forEach(n=>{
+ var node = myDiagram.findNodeForKey(n) as go.Node;
+ if(node === null){
+ console.error("No such node found in Diagram");
+ return;
+ }
+ containGroupKey = node.containingGroup?.key;
+ (node.diagram as go.Diagram).model.setDataProperty(node.data, "group", name);
+ });
+
+ // 如果发现恢复的Group内部没有任何成员可见,那么折叠这个Group并使其不可见
+ if(parent.memberParts.all(m => !m.isVisible())){
+ (parent.diagram as go.Diagram).model.setDataProperty(parent, "isSubGraphExpanded", false);
+ (parent.diagram as go.Diagram).model.setDataProperty(parent, "visible", false);
+ }
+
+ // 设置恢复Group的containingGroup
+ (parent.diagram as go.Diagram).model.setDataProperty(parent.data, "group", containGroupKey);
+}
+
+// ====================== HighlightPathButton ====================
+
+// todo: 找到对所有路径的合适可视化方式
+function HighlightPathButton(text: string, graph: Graph, highlight: (graph : Graph) => void){
+ var button = document.createElement("button");
+ button.innerHTML = text;
+ document.getElementById("buttonContainer")?.appendChild(button);
+ button.addEventListener("click", function(){
+ highlight(graph);
+ })
+}
+
+function highlightRecommandedPath(graph: Graph){
+ myDiagram.startTransaction("highlight recommanded paths");
+ graph.recommendedPaths.forEach(path=>{
+ // path.forEach(n=>{
+ // let node = myDiagram.findNodeForKey(graph.metadata.getvf(n));
+ // if(node !== null) {
+ // node.diagram?.model.setDataProperty(node.data, "isHighlighted", true);
+ // }
+ // })
+ for(var n = 0; n < path.length - 1; n++){
+ const currNode = myDiagram.findNodeForKey(graph.metadata.getvf(path[n]));
+ const nextNode = myDiagram.findNodeForKey(graph.metadata.getvf(path[n + 1]));
+ if(currNode === null || nextNode === null){
+ console.error("Nodes not found, isField-n: " + graph.isField(path[n]) + ", isField-(n+1)" + graph.isField(path[n + 1]));
+ return;
+ }
+
+ currNode.diagram?.model.setDataProperty(currNode, "isHighlighted", true);
+ currNode.diagram?.model.setDataProperty(currNode.data, "visible", true);
+ currNode.diagram?.model.setDataProperty(currNode.data, "isCollapsed", false);
+ var cg = currNode.containingGroup;
+ while(cg !== null && cg.visible !== true){
+ (cg.diagram as go.Diagram).model.setDataProperty(cg, "visible", true);
+ cg = cg.containingGroup;
+ }
+
+
+ currNode.findLinksTo(nextNode).each(l => {
+ l.diagram?.model.setDataProperty(l, "isHighlighted", true);
+ l.diagram?.model.setDataProperty(l, "visible", true);
+ });
+ }
+
+ const lastNode = myDiagram.findNodeForKey(graph.metadata.getvf(path[path.length - 1]));
+ if(lastNode === null){
+ console.error("Nodes not found, isField-n: " + graph.isField(path[path.length - 1]));
+ return;
+ }
+ lastNode.diagram?.model.setDataProperty(lastNode, "isHighlighted", true);
+ lastNode.diagram?.model.setDataProperty(lastNode.data, "visible", true);
+ lastNode.diagram?.model.setDataProperty(lastNode.data, "isCollapsed", false);
+ var cg = lastNode.containingGroup;
+ while(cg !== null && cg.visible !== true){
+ (cg.diagram as go.Diagram).model.setDataProperty(cg, "visible", true);
+ cg = cg.containingGroup;
+ }
+ })
+ myDiagram.commitTransaction("highlight recommanded paths");
+}
+
+// ===================== ResetButton ========================
+
+function ResetButton(graph: Graph, hide: (graph: Graph) => void){
+ var button = document.createElement("button");
+ button.innerHTML = "Reset";
+ document.getElementById("buttonContainer")?.appendChild(button);
+ button.addEventListener("click", function(){
+ hide(graph);
+ })
+}
+
+function hideAll(graph: Graph){
+ myDiagram.startTransaction("hide All");
+ myDiagram.nodes.each(n=>{
+ if(n instanceof go.Node) {
+ n.diagram?.model.setDataProperty(n.data, "isCollapsed", true);
+ n.diagram?.model.setDataProperty(n.data, "visible", false);
+ while(n.containingGroup !== null){
+ let nd = n.containingGroup as go.Group;
+ if(nd.memberParts.any(m => m.isVisible())) break;
+ nd.diagram?.model.setDataProperty(nd, "isSubGraphExpanded", false);
+ nd.diagram?.model.setDataProperty(nd, "visible", false);
+ n = nd;
+ }
+ }
+ });
+ myDiagram.links.each(l=>l.diagram?.model.setDataProperty(l, "visible", false));
+ graph.sourceNodes.forEach(num => {
+ const s = graph.metadata.getvf(num);
+ var n = myDiagram.findNodeForKey(s) as go.Node;
+ n.diagram?.model.setDataProperty(n, "visible", true);
+ while(n.containingGroup !== null){
+ let nd = n.containingGroup as go.Group;
+ nd.diagram?.model.setDataProperty(nd, "isSubGraphExpanded", false);
+ nd.diagram?.model.setDataProperty(nd, "visible", true);
+ n = nd;
+ }
+ })
+ myDiagram.commitTransaction("hide All");
+ // visualize(graph);
+}
+
+// ===============================================================
+// ===============================================================
+
+myDiagram.click = e => {
+ e.diagram.commit(d => d.clearHighlighteds(), "clear highlights");
+}
+
+// ====================== Fold & Expand Event ====================
+function collapseFrom(node: go.Node, start: go.Node) {
+ // 若该节点还有可见的入边,则保留
+ if (node.findLinksInto().any(link => link.visible) && node !== start){
+ return;
+ }
+
+ if (node.data.isCollapsed) {
+ collapse(node);
+ return;
+ }
+
+ (node.diagram as go.Diagram).model.setDataProperty(node.data, "isCollapsed", true);
+
+ if (node !== start) {
+ collapse(node);
+ }
+ node.findLinksOutOf().each(link => (link.diagram as go.Diagram).model.setDataProperty(link, "visible", false))
+ node.findNodesOutOf().each(n => collapseFrom(n, start));
+
+ function collapse(node: go.Node){
+ (node.diagram as go.Diagram).model.setDataProperty(node.data, "visible", false);
+
+ // 若Group内没有可见的Node,则Group也隐藏
+ var cg = node.containingGroup;
+ while(cg !== null){
+ if (cg.memberParts.any(n => n.visible)) {
+ break;
+ }
+
+ (cg.diagram as go.Diagram).model.setDataProperty(cg, "visible", false);
+ cg = cg.containingGroup;
+ }
+ }
+}
+
+function expandFrom(node: go.Node, start: go.Node) {
+ if (!node.data.isCollapsed)
+ return;
+
+ (node.diagram as go.Diagram).model.setDataProperty(node.data, "isCollapsed", false);
+
+ node.findNodesOutOf().each(n => {
+ (n.diagram as go.Diagram).model.setDataProperty(n.data, "visible", true);
+
+ // 若有Node变为可见,则Node所在的Group也得可见
+ var cg = n.containingGroup;
+ while(cg !== null && cg.visible !== true){
+ (cg.diagram as go.Diagram).model.setDataProperty(cg, "visible", true);
+ cg = cg.containingGroup;
+ }
+ }
+ )
+ node.findLinksOutOf().each(link => (link.diagram as go.Diagram).model.setDataProperty(link, "visible", true))
+}
+// ===============================================================
+
+
+function highlightPath(event: go.InputEvent, ador: go.GraphObject){
+ const diagram = ador.diagram as go.Diagram;
+ const node = ((ador.part as go.Adornment).adornedPart) as go.Node;
+ diagram.startTransaction("highlight");
+ diagram.clearHighlighteds();
+ diagram.findLayer("Foreground")?.parts.each(p=>{
+ if(p instanceof go.Link){
+ p.layerName = "";
+ }
+ })
+ mark(node, 4);
+ diagram.commitTransaction("highlight");
+}
+function mark(node: go.Node, depth: number){
+ if(depth === 0) return;
+
+ node.isHighlighted = true;
+ node.findLinksOutOf().each(l =>{
+ l.isHighlighted = true;
+ l.layerName = "Foreground";
+ });
+ node.findNodesOutOf().each(n => mark(n, depth - 1));
+}
+
+
+
+function makeNodes(graph: Graph){
+ const nodeDataArray: INode[] = [];
+
+ graph.relation.packageToClasses.forEach((vc,kp)=>{
+ const pName:string = graph.metadata.getp(kp)
+
+ nodeDataArray.push({key: pName, isGroup: true, isHighlighted: false, isCollapsed: true});
+ vc.forEach(n=>{
+ if(graph.relation.classToMethods.get(n) !== undefined){
+ nodeDataArray.push({key:graph.metadata.getc(n), isGroup: true, group: pName, isHighlighted: false, isCollapsed: true});
+ }
+ });
+ })
+
+ graph.relation.classToMethods.forEach((vm, kc)=>{
+ const cName:string = graph.metadata.getc(kc);
+ vm.forEach(n=>{
+ if(graph.relation.methodToVars.get(n) !== undefined){
+ nodeDataArray.push({key:graph.metadata.getm(n), isGroup: true, group: cName, isHighlighted: false, isCollapsed: true});
+ }
+ });
+ })
+
+ graph.relation.methodToVars.forEach((vv, km)=>{
+ const mName:string = graph.metadata.getm(km);
+ vv.forEach(n=>{
+ const color:string = graph.isSource(n)? 'gold' : (graph.isSink(n)? 'aquamarine' : 'lightblue');
+ nodeDataArray.push({key: graph.metadata.getvf(n), color: color, group: mName, isHighlighted: false, isCollapsed: true});
+ });
+ })
+ console.log('finish nodes making');
+ return nodeDataArray;
+}
+
+function makeLinks(graph: Graph){
+ const linkDataArray: ILink[] = [];
+
+ graph.graph.forEach((v,k)=>{
+ v.forEach(n=>linkDataArray.push({from: graph.metadata.getvf(k), to: graph.metadata.getvf(n), color: 'black', isHighlighted: false, zOrder: 0}));
+ })
+ console.log('finish links making');
+ return linkDataArray;
+}
+
+interface INode{
+ key?: string;
+ color?: string;
+ isGroup?: boolean;
+ group?: string;
+ isHighlighted: boolean;
+ isCollapsed: boolean;
+}
+
+interface ILink{
+ key?: string;
+ from: string;
+ to: string;
+ color?: string;
+ isHighlighted: boolean;
+ zOrder?: number;
+}
\ No newline at end of file
diff --git a/src/main/ts/src/vite-env.d.ts b/src/main/ts/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/src/main/ts/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/main/ts/tsconfig.json b/src/main/ts/tsconfig.json
new file mode 100644
index 000000000..5e17e93b5
--- /dev/null
+++ b/src/main/ts/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}