From aa6db7e820bcb0a964ab95c74270218e290d29cb Mon Sep 17 00:00:00 2001 From: Aryan Gupta Date: Tue, 16 Jun 2026 16:18:04 +1000 Subject: [PATCH] feat: advanced filters, node dragging, performance optimizations, and settings spacing --- package-lock.json | 165 ++++++++++++++++---------------- package.json | 11 ++- src/settings.ts | 31 +++--- src/types.ts | 4 +- src/view.ts | 239 ++++++++++++++++++++++++++++++++-------------- styles.css | 10 +- tsconfig.json | 4 +- 7 files changed, 284 insertions(+), 180 deletions(-) diff --git a/package-lock.json b/package-lock.json index e67c9a1..fab10c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,35 +9,36 @@ "version": "2.4.1", "license": "MIT", "dependencies": { - "3d-force-graph": "^1.77.0", - "three": "^0.177.0", + "3d-force-graph": "^1.80.0", + "three": "^0.184.0", + "three-forcegraph": "^1.43.4", "three-spritetext": "^1.10.0" }, "devDependencies": { "@types/node": "^16.11.6", - "@types/three": "^0.177.0", + "@types/three": "^0.184.1", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", "esbuild": "0.17.3", - "obsidian": "latest", + "obsidian": "^1.13.1", "tslib": "2.4.0", - "typescript": "4.7.4" + "typescript": "^6.0.3" } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", "dev": true, "license": "MIT", "peer": true, @@ -46,9 +47,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.37.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.1.tgz", - "integrity": "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==", + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "dev": true, "license": "MIT", "peer": true, @@ -649,19 +650,18 @@ } }, "node_modules/@types/three": { - "version": "0.177.0", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.177.0.tgz", - "integrity": "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A==", + "version": "0.184.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", + "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", "dev": true, "license": "MIT", "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", - "@types/webxr": "*", - "@webgpu/types": "*", + "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", - "meshoptimizer": "~0.18.1" + "meshoptimizer": "~1.1.1" } }, "node_modules/@types/three/node_modules/@tweenjs/tween.js": { @@ -878,24 +878,17 @@ "license": "ISC", "peer": true }, - "node_modules/@webgpu/types": { - "version": "0.1.61", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.61.tgz", - "integrity": "sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/3d-force-graph": { - "version": "1.77.0", - "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.77.0.tgz", - "integrity": "sha512-w2MlrCeMJxXwhz5gtRZ7mLU4xW5DD2U6VSEfFv8pvnvSNPYPuAIKjbJoZekfv7yFmMaWnNy/2RfRcgC5oGr2KQ==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.80.0.tgz", + "integrity": "sha512-tzI353gW1nXPpnC7VTa3JjMg+3cp77qOLUFO0vucPTfF+q5R6sQsNsIqVTbRIb7RSypn14nBa4yfkOe9ThxASw==", "license": "MIT", "dependencies": { "accessor-fn": "1", "kapsule": "^1.16", - "three": ">=0.118 <1", + "three": ">=0.179 <1", "three-forcegraph": "1", - "three-render-objects": "^1.35" + "three-render-objects": "^1.41" }, "engines": { "node": ">=12" @@ -936,9 +929,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "peer": true, @@ -1008,9 +1001,9 @@ "peer": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "peer": true, @@ -1768,9 +1761,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC", "peer": true @@ -2020,10 +2013,20 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "peer": true, "dependencies": { @@ -2113,9 +2116,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -2137,9 +2140,9 @@ } }, "node_modules/meshoptimizer": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "dev": true, "license": "MIT" }, @@ -2158,9 +2161,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -2235,9 +2238,9 @@ "license": "BSD-3-Clause" }, "node_modules/obsidian": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", - "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz", + "integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==", "dev": true, "license": "MIT", "dependencies": { @@ -2245,8 +2248,8 @@ "moment": "2.29.4" }, "peerDependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" } }, "node_modules/once": { @@ -2371,9 +2374,9 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -2396,9 +2399,9 @@ } }, "node_modules/preact": { - "version": "10.26.8", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz", - "integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==", + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -2602,9 +2605,9 @@ } }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, "license": "MIT", "peer": true @@ -2632,15 +2635,15 @@ "peer": true }, "node_modules/three": { - "version": "0.177.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.177.0.tgz", - "integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==", + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", "license": "MIT" }, "node_modules/three-forcegraph": { - "version": "1.42.13", - "resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.42.13.tgz", - "integrity": "sha512-BoG5fB3nlAFeIyiLuFquvWIjt8DA2gdPWlqW/8V8xQcEO7otMmeN2/WWHCP7cWzKEImULxpJ6bNLmmt7TTJaiw==", + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.43.4.tgz", + "integrity": "sha512-FtmiZP/T16ZQaHza3JDaDn0YTXFtg9e7pGnTeU8nzu0NNkx7MpWbF/GvmpbQsWHx3rukHtkRv1fTorLPB3FDEA==", "license": "MIT", "dependencies": { "accessor-fn": "1", @@ -2662,9 +2665,9 @@ } }, "node_modules/three-render-objects": { - "version": "1.40.2", - "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.40.2.tgz", - "integrity": "sha512-4LAW9HJS1XcFN4+ujAWrcGAa3UalVTrtXzeWIR9hgJnYSCDBFgGzok9cDP9sXMlw5SjtDWkH6VOnGont+RzfSw==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.42.0.tgz", + "integrity": "sha512-KYfkPrYGEbIK8ChFocWqOF1aAN80FBUBWVYB8mB2oBpVuVN+52FvvngVYB5ieFANQu7Rt21rPYZ/xKaAgVWWRQ==", "license": "MIT", "dependencies": { "@tweenjs/tween.js": "18 - 25", @@ -2677,7 +2680,7 @@ "node": ">=12" }, "peerDependencies": { - "three": ">=0.168" + "three": ">=0.179" } }, "node_modules/three-spritetext": { @@ -2770,9 +2773,9 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2780,7 +2783,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uri-js": { diff --git a/package.json b/package.json index ea4f86e..130aeeb 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,19 @@ "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", - "@types/three": "^0.177.0", + "@types/three": "^0.184.1", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", "esbuild": "0.17.3", - "obsidian": "latest", + "obsidian": "^1.13.1", "tslib": "2.4.0", - "typescript": "4.7.4" + "typescript": "^6.0.3" }, "dependencies": { - "3d-force-graph": "^1.77.0", - "three": "^0.177.0", + "3d-force-graph": "^1.80.0", + "three": "^0.184.0", + "three-forcegraph": "^1.43.4", "three-spritetext": "^1.10.0" } } diff --git a/src/settings.ts b/src/settings.ts index 59fdf50..ff85530 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -52,8 +52,8 @@ export class Graph3DSettingsTab extends PluginSettingTab { .addOption('path', 'Path') .addOption('tag', 'Tag') .setValue(filter.type) - .onChange(async (value: 'path' | 'tag') => { - filter.type = value; + .onChange(async (value: string) => { + filter.type = value as 'path' | 'tag'; await this.plugin.saveSettings(); this.triggerUpdate({ redrawData: true, useCache: true }); })) @@ -65,14 +65,15 @@ export class Graph3DSettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); this.triggerUpdate({ redrawData: true, useCache: true }); }, 500, true))) - .addToggle(toggle => toggle - .setTooltip("Invert filter (NOT)") - .setValue(filter.inverted) - .onChange(async (value) => { - filter.inverted = value; - await this.plugin.saveSettings(); - this.triggerUpdate({ redrawData: true, useCache: true }); - })) + .addToggle(toggle => { + toggle.setTooltip("Enable/Disable filter") + .setValue(filter.enabled) + .onChange(async (value) => { + filter.enabled = value; + await this.plugin.saveSettings(); + this.triggerUpdate({ redrawData: true, useCache: true }); + }); + }) .addExtraButton(button => button .setIcon('cross') .setTooltip('Remove filter') @@ -88,7 +89,7 @@ export class Graph3DSettingsTab extends PluginSettingTab { .addButton(button => button .setButtonText('Add new filter') .onClick(async () => { - this.plugin.settings.filters.push({ type: 'path', value: '', inverted: false }); + this.plugin.settings.filters.push({ type: 'path', value: '', enabled: true }); await this.plugin.saveSettings(); this.display(); })); @@ -158,11 +159,11 @@ export class Graph3DSettingsTab extends PluginSettingTab { new Setting(containerEl).setName('Appearance').setHeading(); new Setting(containerEl).setName('Node shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.plugin.settings.nodeShape) - .onChange(async(value: NodeShape) => {this.plugin.settings.nodeShape = value; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); + .onChange(async(value: string) => {this.plugin.settings.nodeShape = value as NodeShape; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); new Setting(containerEl).setName('Tag shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.plugin.settings.tagShape) - .onChange(async(value: NodeShape) => {this.plugin.settings.tagShape = value; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); + .onChange(async(value: string) => {this.plugin.settings.tagShape = value as NodeShape; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); new Setting(containerEl).setName('Attachment shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.plugin.settings.attachmentShape) - .onChange(async(value: NodeShape) => {this.plugin.settings.attachmentShape = value; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); + .onChange(async(value: string) => {this.plugin.settings.attachmentShape = value as NodeShape; await this.plugin.saveSettings(); this.triggerUpdate({updateDisplay: true})})); new Setting(containerEl).setName('Node size').addSlider(s => s.setLimits(0.1, 5, 0.1).setValue(this.plugin.settings.nodeSize).setDynamicTooltip() .onChange(async (v) => { this.plugin.settings.nodeSize = v; await this.plugin.saveSettings(); this.triggerUpdate({ updateDisplay: true }); })); new Setting(containerEl).setName('Tag node size').addSlider(s => s.setLimits(0.1, 5, 0.1).setValue(this.plugin.settings.tagNodeSize).setDynamicTooltip() @@ -175,6 +176,8 @@ export class Graph3DSettingsTab extends PluginSettingTab { new Setting(containerEl).setName('Labels').setHeading(); new Setting(containerEl).setName('Show node labels').addToggle(toggle => toggle.setValue(this.plugin.settings.showNodeLabels) .onChange(async (value) => { this.plugin.settings.showNodeLabels = value; await this.plugin.saveSettings(); this.triggerUpdate({ updateDisplay: true }); })); + new Setting(containerEl).setName('Show labels on hover/highlight only').addToggle(toggle => toggle.setValue(this.plugin.settings.showLabelsOnHoverOnly) + .onChange(async (value) => { this.plugin.settings.showLabelsOnHoverOnly = value; await this.plugin.saveSettings(); this.triggerUpdate({ updateDisplay: true }); })); new Setting(containerEl).setName('Label distance').addSlider(s => s.setLimits(50, 500, 10).setValue(this.plugin.settings.labelDistance).setDynamicTooltip() .onChange(async (v) => { this.plugin.settings.labelDistance = v; await this.plugin.saveSettings(); })); new Setting(containerEl).setName('Label fade threshold').addSlider(s => s.setLimits(0.1, 1, 0.1).setValue(this.plugin.settings.labelFadeThreshold).setDynamicTooltip() diff --git a/src/types.ts b/src/types.ts index 2c126a8..d3ca2c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,7 @@ export interface ColorGroup { export interface Filter { type: 'path' | 'tag'; value: string; - inverted: boolean; + enabled: boolean; } export enum NodeShape { Sphere = 'Sphere', Cube = 'Cube', Pyramid = 'Pyramid', Tetrahedron = 'Tetrahedron' } @@ -42,6 +42,7 @@ export interface Graph3DPluginSettings { attachmentShape: NodeShape; // Labels showNodeLabels: boolean; + showLabelsOnHoverOnly: boolean; labelDistance: number; labelFadeThreshold: number; labelTextSize: number; @@ -92,6 +93,7 @@ export const DEFAULT_SETTINGS: Graph3DPluginSettings = { attachmentShape: NodeShape.Cube, // Labels showNodeLabels: true, + showLabelsOnHoverOnly: false, labelDistance: 150, labelFadeThreshold: 0.8, labelTextSize: 2.5, diff --git a/src/view.ts b/src/view.ts index aa3c778..c74c272 100644 --- a/src/view.ts +++ b/src/view.ts @@ -1,5 +1,5 @@ // src/view.ts -import { ItemView, WorkspaceLeaf, TFile, Setting, setIcon, debounce } from 'obsidian'; +import { ItemView, WorkspaceLeaf, TFile, Setting, setIcon, debounce, getAllTags } from 'obsidian'; import ForceGraph3D from '3d-force-graph'; import * as THREE from 'three'; import SpriteText from 'three-spritetext'; @@ -57,12 +57,53 @@ export class Graph3DView extends ItemView { // Keyboard controls state private pressedKeys = new Set(); + private fileContentCache = new Map(); + private frustum = new THREE.Frustum(); + private projScreenMatrix = new THREE.Matrix4(); + private geometryCache = new Map(); + private materialCache = new Map(); + private controlsListenerAdded = false; + constructor(leaf: WorkspaceLeaf, plugin: Graph3DPlugin) { super(leaf); this.plugin = plugin; this.settings = plugin.settings; } + private getSharedGeometry(shape: NodeShape): THREE.BufferGeometry { + let geometry = this.geometryCache.get(shape); + if (!geometry) { + switch (shape) { + case NodeShape.Cube: geometry = new THREE.BoxGeometry(1, 1, 1); break; + case NodeShape.Pyramid: geometry = new THREE.ConeGeometry(1 / 1.5, 1, 4); break; + case NodeShape.Tetrahedron: geometry = new THREE.TetrahedronGeometry(1 / 1.2); break; + default: geometry = new THREE.SphereGeometry(0.5); + } + this.geometryCache.set(shape, geometry); + } + return geometry; + } + + private getSharedMaterial(color: string): THREE.MeshLambertMaterial { + let material = this.materialCache.get(color); + if (!material) { + material = new THREE.MeshLambertMaterial({ + color: color, + transparent: true, + opacity: 0.9 + }); + this.materialCache.set(color, material); + } + return material; + } + + private clearResourceCaches() { + this.geometryCache.forEach(g => g.dispose()); + this.geometryCache.clear(); + this.materialCache.forEach(m => m.dispose()); + this.materialCache.clear(); + } + getViewType() { return VIEW_TYPE_3D_GRAPH; } getDisplayText() { return "3d graph"; } @@ -94,6 +135,17 @@ export class Graph3DView extends ItemView { private addLocalControls() { const controlsContainer = this.contentEl.createEl('div', { cls: 'graph-3d-controls-container' }); + + // Add Reset View button + const resetViewButton = controlsContainer.createEl('div', { cls: 'graph-3d-reset-toggle' }); + setIcon(resetViewButton, 'refresh-cw'); + resetViewButton.setAttribute('aria-label', 'Reset view'); + resetViewButton.addEventListener('click', () => { + if (this.graph && this.isGraphInitialized) { + this.graph.zoomToFit(800); + } + }); + this.settingsToggleButton = controlsContainer.createEl('div', { cls: 'graph-3d-settings-toggle' }); setIcon(this.settingsToggleButton, 'settings'); this.settingsToggleButton.setAttribute('aria-label', 'Graph settings'); @@ -141,9 +193,8 @@ export class Graph3DView extends ItemView { .addDropdown(dropdown => dropdown .addOption('path', 'Path') .addOption('tag', 'Tag') - .setValue(filter.type) - .onChange(async (value: 'path' | 'tag') => { - filter.type = value; + .onChange(async (value: string) => { + filter.type = value as 'path' | 'tag'; await this.plugin.saveSettings(); this.updateData({ useCache: true }); })) @@ -155,14 +206,15 @@ export class Graph3DView extends ItemView { await this.plugin.saveSettings(); this.updateData({ useCache: true }); }, 500, true))) - .addToggle(toggle => toggle - .setTooltip("Invert filter (NOT)") - .setValue(filter.inverted) - .onChange(async (value) => { - filter.inverted = value; - await this.plugin.saveSettings(); - this.updateData({ useCache: true }); - })) + .addToggle(toggle => { + toggle.setTooltip("Enable/Disable filter") + .setValue(filter.enabled) + .onChange(async (value) => { + filter.enabled = value; + await this.plugin.saveSettings(); + this.updateData({ useCache: true }); + }); + }) .addExtraButton(button => button .setIcon('cross') .setTooltip('Remove filter') @@ -178,7 +230,7 @@ export class Graph3DView extends ItemView { .addButton(button => button .setButtonText('Add new filter') .onClick(async () => { - this.settings.filters.push({ type: 'path', value: '', inverted: false }); + this.settings.filters.push({ type: 'path', value: '', enabled: true }); await this.plugin.saveSettings(); this.renderSettingsPanel(); })); @@ -276,11 +328,11 @@ export class Graph3DView extends ItemView { .onChange(async (v) => { this.settings.linkThickness = v; await updateDisplayAndColors(); })); new Setting(container).setName('Node shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.settings.nodeShape) - .onChange(async(value: NodeShape) => {this.settings.nodeShape = value; await updateDisplayAndColors()})); + .onChange(async(value: string) => {this.settings.nodeShape = value as NodeShape; await updateDisplayAndColors()})); new Setting(container).setName('Tag shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.settings.tagShape) - .onChange(async(value: NodeShape) => {this.settings.tagShape = value; await updateDisplayAndColors()})); + .onChange(async(value: string) => {this.settings.tagShape = value as NodeShape; await updateDisplayAndColors()})); new Setting(container).setName('Attachment shape').addDropdown(dd => dd.addOptions(NodeShape).setValue(this.settings.attachmentShape) - .onChange(async(value: NodeShape) => {this.settings.attachmentShape = value; await updateDisplayAndColors()})); + .onChange(async(value: string) => {this.settings.attachmentShape = value as NodeShape; await updateDisplayAndColors()})); } private renderLabelSettings(container: HTMLElement) { @@ -300,6 +352,15 @@ export class Graph3DView extends ItemView { this.updateDisplay(); })); + new Setting(container) + .setName('Show labels on hover/highlight only') + .addToggle(toggle => toggle.setValue(this.settings.showLabelsOnHoverOnly) + .onChange(async (value) => { + this.settings.showLabelsOnHoverOnly = value; + await this.plugin.saveSettings(); + this.updateDisplay(); + })); + new Setting(container) .setName('Label distance') .addSlider(s => s.setLimits(50, 500, 10).setValue(this.settings.labelDistance).setDynamicTooltip() @@ -471,6 +532,19 @@ export class Graph3DView extends ItemView { const Graph = (ForceGraph3D as any).default || ForceGraph3D; this.graph = Graph()(this.graphContainer) + .enableNodeDrag(true) + .onNodeDrag((node: GraphNode) => { + const controls = this.graph.controls(); + if (controls) { + controls.enabled = false; + } + }) + .onNodeDragEnd((node: GraphNode) => { + const controls = this.graph.controls(); + if (controls) { + controls.enabled = true; + } + }) .onNodeClick((node: GraphNode, event: MouseEvent) => this.handleNodeClick(node, event)) .onNodeHover((node: GraphNode | null) => this.handleNodeHover(node)) .linkCurvature((link: ProcessedGraphLink) => this.getLinkCurvature(link)) @@ -580,22 +654,13 @@ export class Graph3DView extends ItemView { this.graph.resumeAnimation(); } else { - if (this.graph && typeof this.graph._destructor === 'function') { - this.graph._destructor(); - } - const Graph = (ForceGraph3D as any).default || ForceGraph3D; - this.graph = Graph()(this.graphContainer) - .onNodeClick((node: GraphNode) => this.handleNodeClick(node)) - .graphData({ nodes: [], links: [] }); - - this.initializeForces(); - + this.graph.graphData({ nodes: [], links: [] }); this.colorCache.clear(); const bgColor = this.settings.useThemeColors ? this.getCssColor('--background-primary', '#000000') : this.settings.backgroundColor; this.graph.backgroundColor(bgColor); - this.messageEl.setText("No search results found."); + this.messageEl.setText("No search results or filters matched."); this.messageEl.addClass('is-visible'); this.graph.pauseAnimation(); } @@ -616,14 +681,10 @@ export class Graph3DView extends ItemView { this.graph.graphData().nodes.forEach((node: GraphNode) => { const mesh = this.nodeMeshes.get(node); - if (mesh && mesh.material) { + if (mesh) { const color = this.getNodeColor(node); if (color) { - try { - (mesh.material as THREE.MeshLambertMaterial).color.set(color); - } catch (e) { - console.error(`3D Graph: Invalid color '${color}' for node`, node, e); - } + mesh.material = this.getSharedMaterial(color); } } }); @@ -758,31 +819,13 @@ export class Graph3DView extends ItemView { default: shape = this.settings.nodeShape; size = this.settings.nodeSize; } - let geometry: THREE.BufferGeometry; + const geometry = this.getSharedGeometry(shape); const s = size * 1.5; - switch (shape) { - case NodeShape.Cube: geometry = new THREE.BoxGeometry(s, s, s); break; - case NodeShape.Pyramid: geometry = new THREE.ConeGeometry(s / 1.5, s, 4); break; - case NodeShape.Tetrahedron: geometry = new THREE.TetrahedronGeometry(s / 1.2); break; - default: geometry = new THREE.SphereGeometry(s / 2); - } - const color = this.getNodeColor(node); - const material = new THREE.MeshLambertMaterial({ - color: '#ffffff', - transparent: true, - opacity: 0.9 - }); + const mesh = new THREE.Mesh(geometry, this.getSharedMaterial(color)); + mesh.scale.set(s, s, s); - try { - material.color.set(color); - } catch (e) { - console.error(`3D Graph: Could not set material color to '${color}'`, e); - material.color.set(this.settings.colorNode); - } - - const mesh = new THREE.Mesh(geometry, material); this.nodeMeshes.set(node, mesh); group.add(mesh); @@ -824,6 +867,10 @@ export class Graph3DView extends ItemView { controls.rotateSpeed = rotateSpeed; controls.panSpeed = panSpeed; controls.zoomSpeed = zoomSpeed; + if (!this.controlsListenerAdded) { + controls.addEventListener('change', () => this.updateLabels()); + this.controlsListenerAdded = true; + } } } @@ -835,19 +882,32 @@ export class Graph3DView extends ItemView { if (!nodes || !camera) return; + this.projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + this.frustum.setFromProjectionMatrix(this.projScreenMatrix); + if (this.settings.labelOcclusion && this.occludersCacheDirty) { this.cachedOccluders = nodes.map((n: GraphNode) => this.nodeMeshes.get(n)).filter(Boolean) as THREE.Mesh[]; this.occludersCacheDirty = false; } + const raycastCullDistanceSq = this.RAYCAST_CULL_DISTANCE * this.RAYCAST_CULL_DISTANCE; const relevantOccluders = this.settings.labelOcclusion - ? this.cachedOccluders.filter(mesh => camera.position.distanceTo(mesh.position) < this.RAYCAST_CULL_DISTANCE) + ? this.cachedOccluders.filter(mesh => camera.position.distanceToSquared(mesh.position) < raycastCullDistanceSq) : []; nodes.forEach((node: GraphNode) => { const sprite = this.nodeSprites.get(node); if (!sprite) return; + if (this.settings.showLabelsOnHoverOnly) { + const isHovered = this.hoveredNode && this.hoveredNode.id === node.id; + const isHighlighted = this.highlightedNodes.has(node.id); + if (!isHovered && !isHighlighted) { + sprite.visible = false; + return; + } + } + if (node.__threeObj) { node.__threeObj.getWorldPosition(this.reusableNodePosition); } else { @@ -855,6 +915,11 @@ export class Graph3DView extends ItemView { return; } + if (!this.frustum.containsPoint(this.reusableNodePosition)) { + sprite.visible = false; + return; + } + const distance = camera.position.distanceTo(this.reusableNodePosition); const visibleDistance = this.settings.labelDistance; @@ -947,6 +1012,7 @@ export class Graph3DView extends ItemView { this.updateColors(); this.graph.linkWidth(this.graph.linkWidth()); this.graph.linkDirectionalParticles(this.graph.linkDirectionalParticles()); + this.updateLabels(); } private handleNodeHover(node: GraphNode | null) { @@ -977,6 +1043,7 @@ export class Graph3DView extends ItemView { this.updateColors(); this.graph.linkWidth(this.graph.linkWidth()); this.graph.linkDirectionalParticles(this.graph.linkDirectionalParticles()); + this.updateLabels(); } private getLinkCurvature(link: ProcessedGraphLink) { @@ -997,6 +1064,9 @@ export class Graph3DView extends ItemView { } if (filter.type === 'tag') { const tagToMatch = filterValue.startsWith('#') ? filterValue.substring(1) : filterValue; + if (node.type === NodeType.Tag) { + return node.name.toLowerCase() === `#${tagToMatch}`; + } return node.tags?.some(tag => tag.toLowerCase() === tagToMatch) ?? false; } return false; @@ -1010,14 +1080,25 @@ export class Graph3DView extends ItemView { const allNodesMap = new Map(); + const needsContent = !!searchQuery || this.settings.groups.some(g => { + const q = g.query.toLowerCase(); + return q && !q.startsWith('path:') && !q.startsWith('tag:') && !q.startsWith('file:'); + }); + for (const file of allFiles) { const cache = this.app.metadataCache.getFileCache(file); - const tags = cache?.tags?.map(t => t.tag.substring(1)) || []; + const tags = cache ? (getAllTags(cache) || []).map(t => t.startsWith('#') ? t.substring(1) : t) : []; const type = file.extension === 'md' ? NodeType.File : NodeType.Attachment; let content = ''; - if (type === NodeType.File) { - content = await this.app.vault.cachedRead(file); + if (type === NodeType.File && needsContent) { + const cached = this.fileContentCache.get(file.path); + if (cached && cached.mtime === file.stat.mtime) { + content = cached.content; + } else { + content = await this.app.vault.cachedRead(file); + this.fileContentCache.set(file.path, { mtime: file.stat.mtime, content }); + } } allNodesMap.set(file.path, { id: file.path, name: file.basename, filename: file.name, type, tags, content }); @@ -1025,9 +1106,29 @@ export class Graph3DView extends ItemView { const allLinks: { source: string, target: string }[] = []; + const existingLinks = new Set(); + for (const sourcePath in resolvedLinks) { for (const targetPath in resolvedLinks[sourcePath]) { allLinks.push({ source: sourcePath, target: targetPath }); + existingLinks.add(`${sourcePath}->${targetPath}`); + } + } + + // Also extract links from frontmatter properties (Properties section) + for (const file of allFiles) { + const cache = this.app.metadataCache.getFileCache(file); + if (cache?.frontmatterLinks) { + for (const linkCache of cache.frontmatterLinks) { + const destFile = this.app.metadataCache.getFirstLinkpathDest(linkCache.link, file.path); + if (destFile) { + const linkKey = `${file.path}->${destFile.path}`; + if (!existingLinks.has(linkKey)) { + allLinks.push({ source: file.path, target: destFile.path }); + existingLinks.add(linkKey); + } + } + } } } @@ -1050,12 +1151,11 @@ export class Graph3DView extends ItemView { let finalNodes = Array.from(allNodesMap.values()); // Advanced Filtering Logic - const positiveFilters = filters.filter(f => !f.inverted && f.value.trim() !== ''); - const negativeFilters = filters.filter(f => f.inverted && f.value.trim() !== ''); + const activeFilters = filters.filter(f => f.enabled && f.value.trim() !== ''); - if (positiveFilters.length > 0) { + if (activeFilters.length > 0) { const nodesToKeep = new Set(); - positiveFilters.forEach(filter => { + activeFilters.forEach(filter => { finalNodes.forEach(node => { if (this.matchesFilter(node, filter)) { nodesToKeep.add(node); @@ -1065,12 +1165,6 @@ export class Graph3DView extends ItemView { finalNodes = Array.from(nodesToKeep); } - if (negativeFilters.length > 0) { - finalNodes = finalNodes.filter(node => { - return !negativeFilters.some(filter => this.matchesFilter(node, filter)); - }); - } - if (searchQuery) { const lowerCaseFilter = searchQuery.toLowerCase(); @@ -1111,12 +1205,7 @@ export class Graph3DView extends ItemView { private cleanupNode(node: GraphNode, options: { cleanMesh?: boolean, cleanGroup?: boolean } = { cleanMesh: true, cleanGroup: true }) { if (options.cleanMesh) { - const mesh = this.nodeMeshes.get(node); - if (mesh) { - mesh.geometry?.dispose(); - (mesh.material as THREE.Material)?.dispose(); - this.nodeMeshes.delete(node); - } + this.nodeMeshes.delete(node); } const sprite = this.nodeSprites.get(node); @@ -1152,5 +1241,7 @@ export class Graph3DView extends ItemView { if (this.messageEl) { this.messageEl.remove(); } + this.clearResourceCaches(); + this.controlsListenerAdded = false; } } diff --git a/styles.css b/styles.css index abdcf18..274a07b 100644 --- a/styles.css +++ b/styles.css @@ -35,9 +35,12 @@ top: 10px; right: 10px; z-index: 10; + display: flex; + gap: 8px; } -.graph-3d-settings-toggle { +.graph-3d-settings-toggle, +.graph-3d-reset-toggle { cursor: pointer; padding: 8px; background-color: var(--background-secondary-alt); @@ -48,7 +51,8 @@ justify-content: center; } -.graph-3d-settings-toggle:hover { +.graph-3d-settings-toggle:hover, +.graph-3d-reset-toggle:hover { background-color: var(--background-modifier-hover); } @@ -73,7 +77,7 @@ .graph-3d-settings-panel .setting-item { border-top: 1px solid var(--background-modifier-border); - padding: 10px 0; + padding: 10px 16px !important; } .graph-3d-settings-panel .setting-item:first-child { diff --git a/tsconfig.json b/tsconfig.json index c44b729..62a0f29 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { - "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", "target": "ES6", "allowJs": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, + "strictPropertyInitialization": false, "lib": [ "DOM", "ES5",