Merge pull request #1 from Apoo711/V1

V1 released!
This commit is contained in:
Apoo711 2025-06-07 13:44:57 +10:00 committed by GitHub
commit 3bed5305ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 3195 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Aryan Gupta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
README.md Normal file
View file

@ -0,0 +1,55 @@
# 3D Graph for Obsidian
A plugin for Obsidian that provides a 3D, force-directed graph view of your vault. This offers an alternative, immersive way to visualize the connections between your notes.
## Features (V1)
- Interactive 3D Canvas: Pan, zoom, and rotate around your notes to explore their relationships from any angle.
- Node Interaction:
- Single-click on a node to focus the camera on it and highlight its immediate connections.
- Double-click on a node to open the corresponding file in a new tab.
- Orphan Hiding: Declutter your view with an option in the plugin settings to hide notes that have no links.
- Live Refresh: The graph updates instantly when you change settings—no need to close and reopen.
## How to Install
1. Download the main.js, manifest.json, and (if you add one) styles.css from the latest release.
2. In Obsidian, open Settings > Community plugins.
3. Make sure "Restricted mode" is turned off.
4. Open your vault's plugin folder by clicking the small folder icon next to "Installed plugins".
5. Create a new folder named 3d-graph-plugin.
6. Copy the downloaded files into this new folder.
7. Go back to Obsidian's "Community plugins" settings and click the refresh button.
8. Enable "3D Graph Plugin".
Once enabled, you can open the 3D Graph from the ribbon icon on the left sidebar.
## Settings
You can configure the 3D Graph by going to `Settings` > `3D Graph Plugin`.
- Show attachments: Toggles the visibility of non-markdown files in the graph.
- Hide orphans: Toggles the visibility of notes with no connections.
## Future Plans (V2)
- Enhanced customization for colors, node sizes, and forces.
- Visual distinction for different file types and tags.
- Performance optimizations for extremely large vaults.
- Local graph mode.
## Acknowledgements
This plugin relies heavily on the fantastic 3d-force-graph library for rendering and physics.
Built with ❤️ for the Obsidian community.

306
main.ts Normal file
View file

@ -0,0 +1,306 @@
// main.ts
import { App, ItemView, Plugin, PluginSettingTab, Setting, WorkspaceLeaf, TFile } from 'obsidian';
// We declare the type for the library which will be loaded onto the window object.
declare const ForceGraph3D: any;
export const VIEW_TYPE_3D_GRAPH = "3d-graph-view";
// --- Define the structure of our plugin's settings ---
interface Graph3DPluginSettings {
showAttachments: boolean;
hideOrphans: boolean; // NEW SETTING
}
// --- Define the default settings ---
const DEFAULT_SETTINGS: Graph3DPluginSettings = {
showAttachments: false,
hideOrphans: false // NEW SETTING
};
class Graph3DView extends ItemView {
private graph: any;
private plugin: Graph3DPlugin;
private settings: Graph3DPluginSettings;
private highlightedNodes = new Set<string>();
private highlightedLinks = new Set<object>();
private selectedNode: string | null = null;
private graphContainer: HTMLDivElement;
private clickTimeout: any = null;
private readonly CLICK_DELAY = 250; // ms
constructor(leaf: WorkspaceLeaf, plugin: Graph3DPlugin) {
super(leaf);
this.plugin = plugin;
this.settings = plugin.settings;
}
getViewType() {
return VIEW_TYPE_3D_GRAPH;
}
getDisplayText() {
return "3D Graph";
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
this.graphContainer = container.createEl('div');
this.graphContainer.style.width = '100%';
this.graphContainer.style.height = '100%';
this.graphContainer.createEl("h4", { text: "Loading 3D Graph library..." });
this.loadScriptAndInitialize();
}
loadScriptAndInitialize() {
if (document.getElementById('3d-force-graph-script')) {
this.initializeGraph();
return;
}
const script = document.createElement('script');
script.id = '3d-force-graph-script';
script.src = 'https://unpkg.com/3d-force-graph';
script.onload = () => this.initializeGraph();
script.onerror = () => {
this.graphContainer.empty();
this.graphContainer.createEl("h4", { text: "Error: Could not load library." });
};
document.body.appendChild(script);
}
initializeGraph() {
this.app.workspace.onLayoutReady(() => {
this.graphContainer.empty();
const graphData = this.processVaultData();
if (!graphData || graphData.nodes.length === 0) {
this.graphContainer.createEl("h4", { text: "No connected files to display." });
return;
}
this.graph = ForceGraph3D()
(this.graphContainer)
.graphData(graphData)
.nodeLabel('name')
.nodeVal(node => 1.5)
.nodeColor(node => this.highlightedNodes.has(node.id as string) ? (node.id === this.selectedNode ? 'yellow' : 'orange') : 'dodgerblue')
.linkColor(link => this.highlightedLinks.has(link) ? 'red' : '#333333')
.linkWidth(link => this.highlightedLinks.has(link) ? 2 : 1)
.onNodeClick(this.handleNodeClick.bind(this));
this.graph.width(this.graphContainer.offsetWidth);
this.graph.height(this.graphContainer.offsetHeight);
});
}
public redrawGraph() {
console.log("3D-Graph: Settings updated, redrawing graph.");
if (this.graph) {
const newData = this.processVaultData();
this.graph.graphData(newData);
}
}
private handleNodeClick(node: any) {
if (!node || !this.graph) return;
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
this.clickTimeout = null;
this.handleNodeDoubleClick(node);
} else {
this.clickTimeout = setTimeout(() => {
this.handleNodeSingleClick(node);
this.clickTimeout = null;
}, this.CLICK_DELAY);
}
}
private handleNodeDoubleClick(node: any) {
const file = this.app.vault.getAbstractFileByPath(node.id as string);
if (file instanceof TFile) {
this.app.workspace.getLeaf('tab').openFile(file);
}
}
private handleNodeSingleClick(node: any) {
if (this.selectedNode === node.id) {
this.selectedNode = null;
this.highlightedNodes.clear();
this.highlightedLinks.clear();
} else {
this.selectedNode = node.id as string;
this.highlightedNodes.clear();
this.highlightedLinks.clear();
this.highlightedNodes.add(node.id as string);
const { links } = this.graph.graphData();
links.forEach((link: any) => {
if (link.source?.id === node.id) {
this.highlightedNodes.add(link.target.id);
this.highlightedLinks.add(link);
} else if (link.target?.id === node.id) {
this.highlightedNodes.add(link.source.id);
this.highlightedLinks.add(link);
}
});
if (node.__threeObj) {
const distance = 100;
const nodePosition = node.__threeObj.position;
this.graph.cameraPosition(
{ x: nodePosition.x, y: nodePosition.y, z: nodePosition.z + distance },
nodePosition,
1000
);
}
}
this.graph.nodeColor(this.graph.nodeColor()).linkColor(this.graph.linkColor()).linkWidth(this.graph.linkWidth());
}
// --- MODIFIED: This function now handles filtering orphans ---
private processVaultData() {
const { showAttachments, hideOrphans } = this.settings;
console.log(`3D-Graph: Settings: showAttachments=${showAttachments}, hideOrphans=${hideOrphans}`);
const files = showAttachments
? this.app.vault.getFiles()
: this.app.vault.getMarkdownFiles();
const resolvedLinks = this.app.metadataCache.resolvedLinks;
if (!resolvedLinks) { return { nodes: [], links: [] }; }
const filePaths = new Set(files.map(f => f.path));
let nodes = files.map(file => ({ id: file.path, name: file.basename }));
const links: { source: string, target: string }[] = [];
for (const sourcePath in resolvedLinks) {
if (!filePaths.has(sourcePath)) continue;
for (const targetPath in resolvedLinks[sourcePath]) {
if (!filePaths.has(targetPath)) continue;
links.push({ source: sourcePath, target: targetPath });
}
}
// --- NEW: Orphan filtering logic ---
if (hideOrphans) {
const linkedNodes = new Set<string>();
links.forEach(link => {
linkedNodes.add(link.source);
linkedNodes.add(link.target);
});
nodes = nodes.filter(node => linkedNodes.has(node.id as string));
}
console.log(`3D-Graph: Found ${nodes.length} nodes and ${links.length} links.`);
return { nodes, links };
}
async onClose() {
console.log("3D-Graph: Closing view and cleaning up WebGL context.");
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
}
if (this.graph) {
this.graph.pauseAnimation();
const renderer = this.graph.renderer();
if (renderer && renderer.domElement) {
renderer.forceContextLoss();
renderer.dispose();
}
this.graph._destructor();
this.graph = null;
}
this.graphContainer?.empty();
}
}
class Graph3DSettingsTab extends PluginSettingTab {
plugin: Graph3DPlugin;
constructor(app: App, plugin: Graph3DPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: '3D Graph Settings' });
new Setting(containerEl)
.setName('Show attachments')
.setDesc('Include attachments (images, PDFs, etc.) in the graph.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showAttachments)
.onChange(async (value) => {
this.plugin.settings.showAttachments = value;
await this.plugin.saveSettings();
}));
// --- NEW: Toggle for hiding orphans ---
new Setting(containerEl)
.setName('Hide orphans')
.setDesc('Do not show notes that have no links.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.hideOrphans)
.onChange(async (value) => {
this.plugin.settings.hideOrphans = value;
await this.plugin.saveSettings();
}));
}
}
export default class Graph3DPlugin extends Plugin {
settings: Graph3DPluginSettings;
async onload() {
console.log("Loading 3D Graph Plugin");
await this.loadSettings();
this.registerView(
VIEW_TYPE_3D_GRAPH,
(leaf) => new Graph3DView(leaf, this)
);
this.addSettingTab(new Graph3DSettingsTab(this.app, this));
this.addRibbonIcon("network", "Open 3D Graph", () => this.activateView());
}
onunload() {
console.log("Unloading 3D Graph Plugin");
this.app.workspace.detachLeavesOfType(VIEW_TYPE_3D_GRAPH);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.app.workspace.getLeavesOfType(VIEW_TYPE_3D_GRAPH).forEach(leaf => {
if (leaf.view instanceof Graph3DView) {
leaf.view.redrawGraph();
}
});
}
async activateView() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE_3D_GRAPH);
const leaf = this.app.workspace.getLeaf('tab');
await leaf.setViewState({
type: VIEW_TYPE_3D_GRAPH,
active: true,
});
this.app.workspace.revealLeaf(leaf);
}
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "3d-graph",
"name": "3D Graph Plugin",
"version": "1.0.0",
"minAppVersion": "1.7.0",
"description": "A 3D Graph Plugin for Obsidian.",
"author": "Aryan Gupta",
"authorUrl": "https://aryan-gupta.is-a.dev",
"isDesktopOnly": true
}

2776
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "3D Graph Plugin",
"version": "1.0.0",
"description": "A 3D Graph Plugin for Obsidian.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "Aryan Gupta",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"3d-force-graph": "^1.77.0"
}
}