feat: advanced filters, node dragging, performance optimizations, and settings spacing

This commit is contained in:
Aryan Gupta 2026-06-16 16:18:04 +10:00
parent 255f613e24
commit aa6db7e820
7 changed files with 284 additions and 180 deletions

165
package-lock.json generated
View file

@ -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": {

View file

@ -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"
}
}

View file

@ -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()

View file

@ -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,

View file

@ -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<string>();
private fileContentCache = new Map<string, { mtime: number, content: string }>();
private frustum = new THREE.Frustum();
private projScreenMatrix = new THREE.Matrix4();
private geometryCache = new Map<NodeShape, THREE.BufferGeometry>();
private materialCache = new Map<string, THREE.MeshLambertMaterial>();
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<string, GraphNode>();
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<string>();
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<GraphNode>();
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;
}
}

View file

@ -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 {

View file

@ -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",