From 861ceeb5d433faba33bebd5c9bb4e28b537e8a06 Mon Sep 17 00:00:00 2001 From: Hananoshika Yomaru Date: Tue, 24 Oct 2023 11:53:34 -0700 Subject: [PATCH] working on setting --- esbuild.config.mjs | 60 ++++---- src/FileManager.ts | 23 ++- src/Interfaces.ts | 16 +- src/PassiveSearchEngine.ts | 50 +++++- src/SettingManager.ts | 3 + src/SettingsSchemas.ts | 2 +- src/graph/Graph.ts | 144 +++++++++++++----- src/graph/Link.ts | 4 + src/graph/Node.ts | 8 +- src/main.ts | 31 ++-- src/settings/SettingTab.ts | 16 +- src/util/waitFor.ts | 1 - src/views/atomics/addSearchInput.ts | 41 +++-- src/views/graph/CenterCoordinates.ts | 2 +- src/views/graph/NewForceGraph.ts | 48 +++--- src/views/graph/NewGraph3dView.ts | 103 ++++++++++--- src/views/settings/GraphSettingsManager.ts | 10 +- .../categories/AddGroupSettingItem.ts | 10 +- .../settings/categories/AddNodeGroupButton.ts | 6 +- .../settings/categories/FilterSettingsView.ts | 24 ++- .../getGraphAfterProcessingConfig.ts | 53 +++++++ .../settings/categories/getMySwitcher.ts | 3 +- 22 files changed, 469 insertions(+), 189 deletions(-) create mode 100644 src/views/settings/categories/getGraphAfterProcessingConfig.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index e4197e9..18ee13f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -11,38 +11,38 @@ if you want to view the source, please visit the github repository of this plugi const prod = process.argv[2] === "production"; const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ["src/main.ts"], - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins, - ], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", + banner: { + js: banner, + }, + entryPoints: ["src/main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", }); if (prod) { - await context.rebuild(); - process.exit(0); + await context.rebuild(); + process.exit(0); } else { - await context.watch(); + await context.watch(); } diff --git a/src/FileManager.ts b/src/FileManager.ts index adbce19..2a3e0e0 100644 --- a/src/FileManager.ts +++ b/src/FileManager.ts @@ -1,4 +1,8 @@ +import { BasicSearchEngine } from "@/BasicSearchEngine"; +import { DvSearchEngine } from "@/DvSearchEngine"; import { IFileManager, IPassiveSearchEngine, IActiveSearchEngine } from "@/Interfaces"; +import { PassiveSearchEngine } from "@/PassiveSearchEngine"; +import { SearchEngineType } from "@/SettingsSchemas"; import Graph3dPlugin from "@/main"; /** @@ -8,9 +12,21 @@ export class MyFileManager implements IFileManager { private plugin: Graph3dPlugin; public searchEngine: IActiveSearchEngine | IPassiveSearchEngine; - constructor(plugin: Graph3dPlugin, searchEngine: IActiveSearchEngine | IPassiveSearchEngine) { + constructor(plugin: Graph3dPlugin) { this.plugin = plugin; - this.searchEngine = searchEngine; + this.setSearchEngine(); + } + + /** + * this will set the search engine base on the setting + */ + setSearchEngine() { + const searchEngine = this.plugin.settingManager.getSettings().pluginSetting.searchEngine; + if (searchEngine === SearchEngineType.default) + this.searchEngine = new BasicSearchEngine(this.plugin); + else if (searchEngine === SearchEngineType.dataview) + this.searchEngine = new DvSearchEngine(this.plugin); + else this.searchEngine = new PassiveSearchEngine(this.plugin); } getFiles() { @@ -26,8 +42,7 @@ export class MyFileManager implements IFileManager { // check whether it is active search engine if (this.searchEngine instanceof IActiveSearchEngine) { return this.searchEngine.searchFiles(this.searchEngine.parseQueryToConfig(query)); - } else { - return this.searchEngine.getFiles(); } + throw new Error("passive search engine cannot search files"); } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 0421d8a..fabcdd2 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -1,4 +1,4 @@ -import { TAbstractFile } from "obsidian"; +import { SearchView, TAbstractFile } from "obsidian"; /** * the config object of the search engine @@ -53,15 +53,11 @@ export abstract class IActiveSearchEngine extends ISearchEngine { } export abstract class IPassiveSearchEngine extends ISearchEngine { - /** - * when the result is change, this callback will be called. - * This mutation should only be fired when the result is stable - */ - abstract mutationCallback: (files: TAbstractFile[]) => void; - /** - * an active way to call it - */ - abstract getFiles(): TAbstractFile[]; + abstract addMutationObserver( + searchResultContainerEl: HTMLDivElement, + view: SearchView, + mutationCallback: (files: TAbstractFile[]) => void + ): void; } export interface IFileManager { diff --git a/src/PassiveSearchEngine.ts b/src/PassiveSearchEngine.ts index 8268019..3977f6c 100644 --- a/src/PassiveSearchEngine.ts +++ b/src/PassiveSearchEngine.ts @@ -1,20 +1,54 @@ import { IPassiveSearchEngine } from "@/Interfaces"; -import { TAbstractFile } from "obsidian"; +import Graph3dPlugin from "@/main"; +import { waitForStable } from "@/util/waitFor"; +import { SearchView, TAbstractFile, TFile } from "obsidian"; + +export type SearchResultFile = ReturnType[0]; + +/** + * given the result from `getResultFromSearchView`, return the files + */ +const getFilesFromSearchResult = (rawSearchResult: unknown) => { + // @ts-ignore + return Array.from(rawSearchResult.keys()) as TFile[]; +}; + +const getResultFromSearchView = async (searchView: SearchView) => { + await waitForStable(() => { + return searchView.dom.resultDomLookup.size; + }, {}); + return searchView.dom.resultDomLookup; +}; /** * this is the built in search engine that uses the obsidian search engine */ export class PassiveSearchEngine implements IPassiveSearchEngine { useBuiltInSearchInput = true; - mutationCallback: (files: TAbstractFile[]) => void; + plugin: Graph3dPlugin; - constructor(mutationCallback: (files: TAbstractFile[]) => void) { - this.mutationCallback = mutationCallback; - - // init the listeners for the changes + constructor(plugin: Graph3dPlugin) { + this.plugin = plugin; } - getFiles(): TAbstractFile[] { - throw new Error("Method not implemented."); + /** + * given a search result container element, add a mutation observer to it + */ + addMutationObserver( + searchResultContainerEl: HTMLDivElement, + view: SearchView, + mutationCallback: (files: TAbstractFile[]) => void + ) { + const observer = new MutationObserver(async (mutations) => { + // if the search result is loading or the cache is not ready, then we know that the search must not be ready yet + if (searchResultContainerEl.classList.contains("is-loading") || !this.plugin.cacheIsReady) + return; + const files = getFilesFromSearchResult(await getResultFromSearchView(view)); + mutationCallback(files); + }); + observer.observe(searchResultContainerEl, { + childList: true, + subtree: true, + }); } } diff --git a/src/SettingManager.ts b/src/SettingManager.ts index d759139..00cb6dd 100644 --- a/src/SettingManager.ts +++ b/src/SettingManager.ts @@ -59,6 +59,9 @@ export class MySettingManager implements ISettingManager { */ private isLoaded = false; + /** + * @remarks don't forget to call `loadSettings` after creating this class + */ constructor(plugin: Graph3dPlugin) { this.plugin = plugin; } diff --git a/src/SettingsSchemas.ts b/src/SettingsSchemas.ts index d9b3bcd..c4ec098 100644 --- a/src/SettingsSchemas.ts +++ b/src/SettingsSchemas.ts @@ -14,7 +14,7 @@ export enum GraphType { export enum SearchEngineType { dataview = "dataview", default = "default", - builtIn = "built-in", + builtIn = "builtIn", } export enum DagOrientation { diff --git a/src/graph/Graph.ts b/src/graph/Graph.ts index 79d349d..80946a3 100644 --- a/src/graph/Graph.ts +++ b/src/graph/Graph.ts @@ -1,6 +1,6 @@ import { Link } from "@/graph/Link"; import { Node } from "@/graph/Node"; -import { App } from "obsidian"; +import { App, TAbstractFile } from "obsidian"; export class Graph { public readonly nodes: Node[]; @@ -77,43 +77,62 @@ export class Graph { } /** + * This method retrieves the local graph of a node, which includes all the other nodes and links + * that are connected to the given node recursively based on the specified depth and link type. * - * @param nodeId * @returns the local graph of a node */ - public getLocalGraph(nodeId: string): Graph { - const node = this.getNodeById(nodeId); - if (node) { - const nodes = [node, ...node.neighbors]; - const links: Link[] = []; - const nodeIndex = new Map(); + public getLocalGraph( + param: ( + | { + id: string; + } + | { + path: string; + } + ) & { + depth: number; + linkType: "both" | "inlinks" | "outlinks"; + } + ): Graph { + const node = "id" in param ? this.getNodeById(param.id) : this.getNodeByPath(param.path); - nodes.forEach((node, index) => { - nodeIndex.set(node.id, index); - }); + const nodes: Node[] = []; + const links: Link[] = []; + const nodeIndex = new Map(); - nodes.forEach((node, index) => { - const filteredLinks = node.links - .filter((link) => nodeIndex.has(link.target.id) && nodeIndex.has(link.source.id)) - .map((link) => { - if ( - !links.includes(link) && - nodeIndex.has(link.target.id) && - nodeIndex.has(link.source.id) - ) - links.push(link); - return link; - }); - - node.links.splice(0, node.links.length, ...filteredLinks); - }); - - const linkIndex = Link.createLinkIndex(links); - - return new Graph(nodes, links, nodeIndex, linkIndex); - } else { + if (!node) { return new Graph([], [], new Map(), new Map()); } + + const traverseNeighbors = (currentNode: Node, currentDepth: number) => { + if (currentDepth > param.depth || nodeIndex.has(currentNode.id)) return; // Depth limit reached or node already traversed + + nodes.push(currentNode); + nodeIndex.set(currentNode.id, nodes.length - 1); + + currentNode.links.forEach((link) => { + if ( + param.linkType === "both" || + (param.linkType === "inlinks" && link.target.id === currentNode.id) || + (param.linkType === "outlinks" && link.source.id === currentNode.id) + ) { + if (!links.includes(link)) { + links.push(link); + traverseNeighbors( + link.target.id === currentNode.id ? link.source : link.target, + currentDepth + 1 + ); + } + } + }); + }; + + traverseNeighbors(node, 0); + + const linkIndex = Link.createLinkIndex(links); + + return new Graph(nodes, links, nodeIndex, linkIndex); } // Clones the graph @@ -126,6 +145,10 @@ export class Graph { ); }; + public static createEmpty = (): Graph => { + return new Graph([], [], new Map(), new Map()); + }; + // Creates a graph using the Obsidian API public static createFromApp = (app: App): Graph => { const [nodes, nodeIndex] = Node.createFromFiles(app.vault.getFiles()), @@ -133,6 +156,16 @@ export class Graph { return new Graph(nodes, links, nodeIndex, linkIndex); }; + public static createFromFiles = (files: TAbstractFile[], app: App): Graph => { + const [nodes, nodeIndex] = Node.createFromFiles(files), + [links, linkIndex] = Link.createFromCache(app.metadataCache.resolvedLinks, nodes, nodeIndex); + const tempGraph = new Graph(nodes, links, nodeIndex, linkIndex); + // const tempGraph = Graph.createFromApp(app); + // since the nodes are from the files, they are already correct. + // we just need to update the links, we can pass into Boolean as the predicate + return this.createFromMask(Boolean, tempGraph); + }; + // updates this graph with new data from the Obsidian API public update = (app: App) => { const newGraph = Graph.createFromApp(app); @@ -153,12 +186,13 @@ export class Graph { /** * filter the nodes of the graph, the links will be filtered automatically. - * @param predicate this + * @param predicate what nodes to keep + * @param graph the graph to filter * @returns a new graph */ - public filter = (predicate: (node: Node) => boolean) => { - const filteredNodes = this.nodes.filter(predicate); - const filteredLinks = this.links.filter((link) => { + public static createFromMask = (predicate: (node: Node) => boolean, graph: Graph) => { + const filteredNodes = graph.nodes.filter(predicate); + const filteredLinks = graph.links.filter((link) => { // the source and target nodes of a link must be in the filtered nodes return ( filteredNodes.some((node) => link.source.id === node.id) && @@ -175,4 +209,44 @@ export class Graph { return new Graph(filteredNodes, filteredLinks, nodeIndex, linkIndex); }; + + public static compare = (graph1: Graph, graph2: Graph): boolean => { + if (graph1.nodes.length !== graph2.nodes.length) { + return false; + } + if (graph1.links.length !== graph2.links.length) { + return false; + } + + const graph2NodeIds = new Set(graph2.nodes.map((node) => node.id)); + + // Check if all nodes in graph1 exist in graph2 + for (const node1 of graph1.nodes) { + if (!graph2NodeIds.has(node1.id)) { + return false; + } + } + + function getLinkId(link: Link): string { + return `${link.source.path}-${link.target.path}`; + } + + const graph2LinkIds = new Set(graph2.links.map(getLinkId)); + + // Check if all links in graph1 exist in graph2 + for (const link1 of graph1.links) { + if (!graph2LinkIds.has(getLinkId(link1))) { + return false; + } + } + + return true; + }; + + /** + * get the files from the graph + */ + public static getFiles(app: App, graph: Graph): TAbstractFile[] { + return graph.nodes.map((node) => app.vault.getAbstractFileByPath(node.path)).filter(Boolean); + } } diff --git a/src/graph/Link.ts b/src/graph/Link.ts index ea77bed..629519b 100644 --- a/src/graph/Link.ts +++ b/src/graph/Link.ts @@ -72,4 +72,8 @@ export class Link { return [links, Link.createLinkIndex(links)]; } + + public static compare = (a: Link, b: Link) => { + return a.source.path === b.source.path && a.target.path === b.target.path; + }; } diff --git a/src/graph/Node.ts b/src/graph/Node.ts index 19fd883..97766ff 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -1,5 +1,5 @@ import { Link } from "@/graph/Link"; -import { TFile } from "obsidian"; +import { TAbstractFile } from "obsidian"; export class Node { public readonly id: string; @@ -20,7 +20,7 @@ export class Node { } // Creates an array of nodes from an array of files (from the Obsidian API) - static createFromFiles(files: TFile[]): [Node[], Map] { + static createFromFiles(files: TAbstractFile[]): [Node[], Map] { const nodeMap = new Map(); return [ files @@ -64,4 +64,8 @@ export class Node { if (node instanceof Node) return this.neighbors.includes(node); else return this.neighbors.some((neighbor) => neighbor.id === node); } + + public static compare = (a: Node, b: Node) => { + return a.path === b.path; + }; } diff --git a/src/main.ts b/src/main.ts index 44e014b..1f9a264 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,16 +10,14 @@ import { eventBus } from "@/util/EventBus"; import { SettingTab } from "@/settings/SettingTab"; import { config } from "@/config"; import { MyFileManager } from "@/FileManager"; -import { BasicSearchEngine } from "@/BasicSearchEngine"; -import { DvSearchEngine } from "@/DvSearchEngine"; import { MySettingManager } from "@/SettingManager"; -import { GraphType, SearchEngineType } from "@/SettingsSchemas"; +import { GraphType } from "@/SettingsSchemas"; import { SearchManager } from "@/SearchManager"; import { NewGraph3dView } from "@/views/graph/NewGraph3dView"; export default class Graph3dPlugin extends Plugin { _resolvedCache: ResolvedLinkCache; - private cacheIsReady: State = new State( + public readonly cacheIsReady: State = new State( this.app.metadataCache.resolvedLinks !== undefined ); /** @@ -42,18 +40,13 @@ export default class Graph3dPlugin extends Plugin { this.settingManager = new MySettingManager(this); // load the setting using setting manager - const settings = await this.settingManager.loadSettings(); + await this.settingManager.loadSettings(); // get the setting from setting manager // const setting = this.settingManager.getSetting("test"); // initalise the file manager - this.fileManager = new MyFileManager( - this, - settings.pluginSetting.searchEngine === SearchEngineType.dataview - ? new DvSearchEngine(this) - : new BasicSearchEngine(this) - ); + this.fileManager = new MyFileManager(this); // init the theme this.theme = new ObsidianTheme(this.app.workspace.containerEl); @@ -110,12 +103,19 @@ export default class Graph3dPlugin extends Plugin { ); } + /** + * this will be called the when the cache is ready. + * And this will hit the else clause of the `onGraphCacheChanged` function + */ private onGraphCacheReady = () => { console.log("Graph cache is ready"); this.cacheIsReady.value = true; this.onGraphCacheChanged(); }; + /** + * check if the cache is ready and if it is, update the global graph + */ public onGraphCacheChanged = () => { // check if the cache actually updated // Obsidian API sends a lot of (for this plugin) unnecessary stuff @@ -133,6 +133,15 @@ export default class Graph3dPlugin extends Plugin { " and ", deepCompare(this._resolvedCache, this.app.metadataCache.resolvedLinks) ); + + // update global graph view + this.activeGraphViews.forEach((view) => { + if (view.graphType === GraphType.global) { + view.updateGraphData({ + files: Graph.getFiles(this.app, this.globalGraph), + }); + } + }); } }; diff --git a/src/settings/SettingTab.ts b/src/settings/SettingTab.ts index 444da45..ebbd792 100644 --- a/src/settings/SettingTab.ts +++ b/src/settings/SettingTab.ts @@ -1,9 +1,6 @@ import { App, PluginSettingTab, Setting } from "obsidian"; import Graph3dPlugin from "@/main"; import { SearchEngineType } from "@/SettingsSchemas"; -import { BasicSearchEngine } from "@/BasicSearchEngine"; -import { DvSearchEngine } from "@/DvSearchEngine"; -import { PassiveSearchEngine } from "@/PassiveSearchEngine"; const DEFAULT_NUMBER = 200; @@ -59,7 +56,9 @@ export class SettingTab extends PluginSettingTab { .setDesc("Search engine determine how to parse the query string and return results.") .addDropdown((dropdown) => { dropdown - .addOptions(SearchEngineType) + .addOptions({ + [SearchEngineType.builtIn]: SearchEngineType.builtIn, + }) // you need to add options before set value .setValue(pluginSetting.searchEngine) .onChange(async (value: SearchEngineType) => { @@ -70,14 +69,7 @@ export class SettingTab extends PluginSettingTab { }); // update the plugin file manager - this.plugin.fileManager.searchEngine = - value === SearchEngineType.default - ? new BasicSearchEngine(this.plugin) - : value === SearchEngineType.dataview - ? new DvSearchEngine(this.plugin) - : new PassiveSearchEngine((files) => { - console.log(files); - }); + this.plugin.fileManager.setSearchEngine(); // force all the graph view to reset their settings this.plugin.activeGraphViews.forEach((view) => view.settingManager.resetSettings()); diff --git a/src/util/waitFor.ts b/src/util/waitFor.ts index 09031c5..a6c6e0a 100644 --- a/src/util/waitFor.ts +++ b/src/util/waitFor.ts @@ -67,7 +67,6 @@ export function waitForStable( clearInterval(intervalId); resolve(currentValue); } - console.log(previousValue, currentValue); previousValue = currentValue; }, interval); }); diff --git a/src/views/atomics/addSearchInput.ts b/src/views/atomics/addSearchInput.ts index e950dbe..c630040 100644 --- a/src/views/atomics/addSearchInput.ts +++ b/src/views/atomics/addSearchInput.ts @@ -1,8 +1,11 @@ -import Graph3dPlugin from "@/main"; +import { PassiveSearchEngine } from "@/PassiveSearchEngine"; +import { NewGraph3dView } from "@/views/graph/NewGraph3dView"; import { spawnLeafView } from "@/views/leafView"; -import { TextComponent } from "obsidian"; +import { SearchView, TAbstractFile, TextComponent } from "obsidian"; /** + * depends on the search engine, this might be a normal text input or a built-in search input + * * @remarks * the search input doesn't perform the search, it only display the input */ @@ -21,12 +24,12 @@ export const addSearchInput = async ( * @param files the files that match the query */ onChange: (value: string) => void, - plugin: Graph3dPlugin + view: NewGraph3dView ) => { const searchEl = containerEl.createDiv({ // cls : }); - if (!plugin.fileManager.searchEngine.useBuiltInSearchInput) { + if (!view.plugin.fileManager.searchEngine.useBuiltInSearchInput) { const text = new TextComponent(searchEl).setValue(value).onChange((value) => { onChange(value); }); @@ -38,7 +41,7 @@ export const addSearchInput = async ( return; } - const [searchLeaf] = spawnLeafView(plugin, searchEl); + const [searchLeaf] = spawnLeafView(view.plugin, searchEl); await searchLeaf.setViewState({ type: "search", @@ -56,7 +59,7 @@ export const addSearchInput = async ( ".search-result-container" ) as HTMLDivElement; const searchResultInfoEl = searchElement.querySelector(".search-results-info") as HTMLDivElement; - // const parentEl = searchResultContainerEl.parentElement!; + searchResultContainerEl.style.visibility = "hidden"; searchResultContainerEl.style.height = "0px"; searchResultContainerEl.style.position = "absolute"; @@ -77,10 +80,9 @@ export const addSearchInput = async ( settingIconEl?.remove(); matchCaseIconEl?.remove(); inputEl.value = value; - inputEl.onkeydown = async (e) => { - // const query = inputEl.value; - // const files = plugin.fileManager.searchFiles(query); - onChange(inputEl.value); + inputEl.oninput = async (e: Event) => { + // @ts-ignore + onChange(e.currentTarget?.value); }; clearButtonEl.onclick = () => { @@ -89,5 +91,22 @@ export const addSearchInput = async ( onChange(""); }; - return [searchRowEl] as const; + // this make search that the search result container el is alaways visible + view.containerEl.appendChild(searchResultContainerEl); + + // if it is a passive engine, we need to enable mutation observer + + const addMutationObserver = (callback: (files: TAbstractFile[]) => void) => { + if (view.plugin.fileManager.searchEngine instanceof PassiveSearchEngine) + view.plugin.fileManager.searchEngine.addMutationObserver( + searchResultContainerEl, + searchLeaf.view as SearchView, + callback + ); + else { + throw new Error("cannot add mutation observer to active search engine"); + } + }; + + return { searchRowEl, addMutationObserver }; }; diff --git a/src/views/graph/CenterCoordinates.ts b/src/views/graph/CenterCoordinates.ts index 3b9b348..e00bc1e 100644 --- a/src/views/graph/CenterCoordinates.ts +++ b/src/views/graph/CenterCoordinates.ts @@ -1,5 +1,5 @@ import * as THREE from "three"; -import { origin } from "./NewForceGraph"; +import { origin } from "@/views/graph/NewForceGraph"; export class CenterCoordinates { public readonly arrowsGroup = new THREE.Group(); diff --git a/src/views/graph/NewForceGraph.ts b/src/views/graph/NewForceGraph.ts index 2ba73a9..6e0cce5 100644 --- a/src/views/graph/NewForceGraph.ts +++ b/src/views/graph/NewForceGraph.ts @@ -13,6 +13,13 @@ import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; */ export const origin = new THREE.Vector3(0, 0, 0); +type MyForceGraph3DInstance = Omit & { + graphData: { + (): Graph; // When no argument is passed, it returns a Graph + (graph: Graph): MyForceGraph3DInstance; // When a Graph is passed, it returns MyForceGraph3DInstance + }; +}; + /** * this class control the config and graph of the force graph. The interaction is not control here. */ @@ -20,15 +27,13 @@ export class NewForceGraph { private view: NewGraph3dView; private config: LocalGraphSettings | GlobalGraphSettings; - private graph: Graph; - private instance: ForceGraph3DInstance; + public readonly instance: MyForceGraph3DInstance; private centerCoordinates: CenterCoordinates; /** * * this will create a new force graph instance and render it to the view * @param view - * @param graph * @param config you have to provide the full config here!! */ constructor( @@ -38,7 +43,7 @@ export class NewForceGraph { ) { this.view = view; this.config = config; - this.graph = graph; + // test; // get the content element of the item view const rootHtmlElement = view.contentEl as HTMLDivElement; @@ -62,12 +67,13 @@ export class NewForceGraph { }), ], })(rootHtmlElement) + .graphData(graph) // the options here are auto .width(rootHtmlElement.innerWidth) .height(rootHtmlElement.innerHeight) .d3Force("collide", d3.forceCollide(5)) // transparent - .backgroundColor(hexToRGBA("#000000", 0)); + .backgroundColor(hexToRGBA("#000000", 0)) as unknown as MyForceGraph3DInstance; const scene = this.instance.scene(); // add others things @@ -94,13 +100,16 @@ export class NewForceGraph { * given a new force Graph, the update the graph and the instance */ public updateGraph(graph: Graph) { - this.updateInstance(graph, undefined); + // some optimization here + // if the graph is the same, then we don't need to update the graph + const same = Graph.compare(this.instance.graphData(), graph); + if (!same) this.updateInstance(graph, undefined); } /** * given the changed things, update the instance */ - public updateInstance = (graph?: Graph, config?: Partial) => { + private updateInstance = (graph?: Graph, config?: Partial) => { if (graph !== undefined) this.instance.graphData(graph); if (config?.display?.nodeSize !== undefined) this.instance.nodeRelSize(config.display?.nodeSize); @@ -120,25 +129,12 @@ export class NewForceGraph { /** * derive the need to reheat the simulation */ - const needReheat = - config?.display?.nodeRepulsion !== undefined || - config?.display?.linkDistance !== undefined || - graph !== undefined; + // const needReheat = + // config?.display?.nodeRepulsion !== undefined || config?.display?.linkDistance !== undefined; - if (needReheat) { - this.instance.numDimensions(3); // reheat simulation - this.instance.refresh(); - } + // if (needReheat) { + // this.instance.numDimensions(3); // reheat simulation + // this.instance.refresh(); + // } }; - - public getInstance() { - /** - * patch the graph data so that the type is right. - * - * this should be the only way to access graph data from a force graph instance - */ - return this.instance as unknown as Omit & { - graphData: () => Graph; - }; - } } diff --git a/src/views/graph/NewGraph3dView.ts b/src/views/graph/NewGraph3dView.ts index 7dc9e9d..3900c11 100644 --- a/src/views/graph/NewGraph3dView.ts +++ b/src/views/graph/NewGraph3dView.ts @@ -2,16 +2,14 @@ import { GraphType } from "@/SettingsSchemas"; import { config } from "@/config"; import { Graph } from "@/graph/Graph"; import Graph3dPlugin from "@/main"; +import { createNotice } from "@/util/createNotice"; import { NewForceGraph } from "@/views/graph/NewForceGraph"; import { GraphSettingManager } from "@/views/settings/GraphSettingsManager"; +import { getGraphAfterProcessingConfig } from "@/views/settings/categories/getGraphAfterProcessingConfig"; import { App, ItemView, TAbstractFile, WorkspaceLeaf } from "obsidian"; -/** - * This is the entry point for the graph view - */ -const getGraph = (app: App): Graph => { - // TODO: implement this - return Graph.createFromApp(app); +const getGraphFromFiles = (app: App, files: TAbstractFile[]): Graph => { + return Graph.createFromFiles(files, app); }; export class NewGraph3dView extends ItemView { @@ -25,7 +23,7 @@ export class NewGraph3dView extends ItemView { /** * if this is the local graph, this will be the current file ; */ - private currentFile: TAbstractFile; + public currentFile: TAbstractFile | undefined; public readonly settingManager: GraphSettingManager; @@ -43,13 +41,14 @@ export class NewGraph3dView extends ItemView { if (file) { this.currentFile = file; - // recreate the graph view - const graph = getGraph(this.plugin.app); - this.forceGraph = new NewForceGraph( - this, - graph, - this.plugin.settingManager.getNewSetting(graphType) - ); + const graph = getGraphAfterProcessingConfig(this.plugin, { + files: [], + graphType: GraphType.local, + setting: this.settingManager.getCurrentSetting().filter, + centerFile: file, + }); + + this.updateGraphData({ graph }); // we need to append the setting so that setting will be in front of the graph this.contentEl.appendChild(this.settingManager.containerEl); @@ -62,20 +61,29 @@ export class NewGraph3dView extends ItemView { } } - console.warn("temporarily disabled"); - // the view is already initialized, so we can create the graph + // set up some UI stuff + this.contentEl.classList.add("graph-3d-view"); + this.settingManager = new GraphSettingManager(this); + + const graph = + this.graphType === GraphType.local + ? getGraphAfterProcessingConfig(this.plugin, { + files: [], + graphType: GraphType.local, + setting: this.settingManager.getCurrentSetting().filter, + centerFile: this.currentFile, + }) + : this.plugin.globalGraph; // you need to set up the graph before setting so that the setting will be in front of the graph - const graph = getGraph(this.plugin.app); this.forceGraph = new NewForceGraph( this, graph, this.plugin.settingManager.getNewSetting(graphType) ); - // set up some UI stuff - this.contentEl.classList.add("graph-3d-view"); - this.settingManager = new GraphSettingManager(this); + // move the setting to the front of the graph + this.contentEl.appendChild(this.settingManager.containerEl); } onload(): void { @@ -85,7 +93,7 @@ export class NewGraph3dView extends ItemView { onunload(): void { super.onunload(); - this.forceGraph.getInstance()._destructor(); + this.forceGraph?.instance._destructor(); this.plugin.activeGraphViews = this.plugin.activeGraphViews.filter((view) => view !== this); } @@ -106,15 +114,60 @@ export class NewGraph3dView extends ItemView { if (this.forceGraph) this.forceGraph.updateDimensions(); } + /** + * get the current force graph object + */ public getForceGraph() { return this.forceGraph; } + /** + * destroy the old graph, remove the old graph completely from the DOM. + * reassign a new graph base on setting like the constructor, + * then render it. + */ public refreshGraph() { - // destroy the old graph, remove the old graph completely from the DOM - // reassign a new graph base on setting like the constructor - // then render it + const graph = this.forceGraph.instance.graphData(); - throw new Error("not implemented yet"); + // get the first child of the content element + const forceGraphEl = this.contentEl.firstChild; + forceGraphEl?.remove(); + + // destroy the old graph, remove the old graph completely from the DOM + this.forceGraph.instance._destructor(); + + // reassign a new graph base on setting like the constructor + this.forceGraph = new NewForceGraph(this, graph, this.settingManager.getCurrentSetting()); + + // move the setting to the front of the graph + this.contentEl.appendChild(this.settingManager.containerEl); + + this.onResize(); } + + /** + * given some files and config, update the graph data. + */ + public updateGraphData( + param: + | { + files: TAbstractFile[]; + } + | { + graph: Graph; + } + ) { + const graph = "files" in param ? getGraphFromFiles(this.app, param.files) : param.graph; + const tooLarge = + graph.nodes.length > this.plugin.settingManager.getSettings().pluginSetting.maxNodeNumber; + if (tooLarge) { + createNotice(`Graph is too large to be rendered. Have ${graph.nodes.length} nodes.`); + } + this.forceGraph.updateGraph(tooLarge ? Graph.createEmpty() : graph); + } + + /** + * handle setting update + */ + public handleSettingUpdate = () => {}; } diff --git a/src/views/settings/GraphSettingsManager.ts b/src/views/settings/GraphSettingsManager.ts index 9fbedc6..7d2801c 100644 --- a/src/views/settings/GraphSettingsManager.ts +++ b/src/views/settings/GraphSettingsManager.ts @@ -20,6 +20,8 @@ export class GraphSettingManager { public readonly containerEl: HTMLDivElement; private settingsButton: ExtraButtonComponent; private graphControlsEl: HTMLDivElement; + + public filterSettingView: ReturnType; public displaySettingView: ReturnType; private currentSetting: State; @@ -59,7 +61,10 @@ export class GraphSettingManager { this.graphControlsEl, this.currentSetting.value.filter, "Filters", - (...args) => FilterSettingsView(...args, this) + (...args) => { + this.filterSettingView = FilterSettingsView(...args, this); + return this.filterSettingView; + } ); // add the group settings @@ -69,6 +74,7 @@ export class GraphSettingManager { "Groups", (...args) => GroupSettingsView(...args, this.graphView) ); + this.appendSettingGroup( this.graphControlsEl, this.currentSetting.value, @@ -173,6 +179,8 @@ export class GraphSettingManager { // pass the updated graph object or any other new config to update the graph } + this.graphView.handleSettingUpdate(); + return this.currentSetting.value; } diff --git a/src/views/settings/categories/AddGroupSettingItem.ts b/src/views/settings/categories/AddGroupSettingItem.ts index dcb7226..0fd97fd 100644 --- a/src/views/settings/categories/AddGroupSettingItem.ts +++ b/src/views/settings/categories/AddGroupSettingItem.ts @@ -17,11 +17,10 @@ export const AddNodeGroupItem = async ( */ index: number ) => { - const plugin = view.plugin; // This group must exist const groupEl = containerEl.createDiv({ cls: "graph-color-group" }); - await addSearchInput( + const searchInput = await addSearchInput( groupEl, newGroup.query, (value) => { @@ -31,9 +30,14 @@ export const AddNodeGroupItem = async ( return setting; }); }, - plugin + view ); + if (searchInput) + searchInput.addMutationObserver((files) => { + console.log("set the graph config", files); + }); + addColorPicker(groupEl, newGroup.color, (value) => { view.settingManager.updateCurrentSettings((setting) => { // This group must exist diff --git a/src/views/settings/categories/AddNodeGroupButton.ts b/src/views/settings/categories/AddNodeGroupButton.ts index e2ed0e3..79f8c52 100644 --- a/src/views/settings/categories/AddNodeGroupButton.ts +++ b/src/views/settings/categories/AddNodeGroupButton.ts @@ -30,11 +30,11 @@ export const addNodeGroupButton = ( // add a group to group settings view.settingManager.updateCurrentSettings((setting) => { setting.groups.push(newGroup); + // add a group to UI as well, add it in the containerEl before the button container el return setting; }); - - // add a group to UI as well, add it in the containerEl before the button container el - const index = groupSettings.length - 1; + // we need to get the latest current setting so that index will be correct + const index = view.settingManager.getCurrentSetting().groups.length - 1; AddNodeGroupItem(newGroup, containerEl, view, index); containerEl.append(buttonContainerEl); }); diff --git a/src/views/settings/categories/FilterSettingsView.ts b/src/views/settings/categories/FilterSettingsView.ts index c04f7fd..07ae10c 100644 --- a/src/views/settings/categories/FilterSettingsView.ts +++ b/src/views/settings/categories/FilterSettingsView.ts @@ -4,6 +4,7 @@ import { addSearchInput } from "@/views/atomics/addSearchInput"; import { NewGraph3dView } from "@/views/graph/NewGraph3dView"; import { BaseFilterSettings, LocalFilterSetting, LocalGraphSettings } from "@/SettingManager"; import { GraphType } from "@/SettingsSchemas"; +import { getGraphAfterProcessingConfig } from "@/views/settings/categories/getGraphAfterProcessingConfig"; export const FilterSettingsView = async ( filterSettings: BaseFilterSettings | LocalFilterSetting, @@ -11,8 +12,7 @@ export const FilterSettingsView = async ( settingManager: NewGraph3dView["settingManager"] ) => { const graphView = settingManager.getGraphView(); - const plugin = graphView.plugin; - await addSearchInput( + const searchInput = await addSearchInput( containerEl, filterSettings.searchQuery, (value) => { @@ -22,9 +22,23 @@ export const FilterSettingsView = async ( return setting; }); }, - plugin + graphView ); + // if this is a built-in search input, then we need to add a mutation observer + if (searchInput) + searchInput.addMutationObserver((files) => { + // the files is empty, by default, we will show all files + graphView.updateGraphData({ + graph: getGraphAfterProcessingConfig(graphView.plugin, { + files, + graphType: graphView.graphType, + setting: settingManager.getCurrentSetting().filter, + centerFile: graphView.currentFile, + }), + }); + }); + // add show attachments setting new Setting(containerEl).setName("Show Attachments").addToggle((toggle) => { toggle.setValue(filterSettings.showAttachments || false).onChange(async (value) => { @@ -82,5 +96,9 @@ export const FilterSettingsView = async ( settingManager.displaySettingView.showDagOrientationSetting(); }); }); + + return { + searchInput, + }; } }; diff --git a/src/views/settings/categories/getGraphAfterProcessingConfig.ts b/src/views/settings/categories/getGraphAfterProcessingConfig.ts new file mode 100644 index 0000000..1716558 --- /dev/null +++ b/src/views/settings/categories/getGraphAfterProcessingConfig.ts @@ -0,0 +1,53 @@ +import { TAbstractFile } from "obsidian"; +import { BaseFilterSettings, LocalFilterSetting } from "@/SettingManager"; +import { GraphType } from "@/SettingsSchemas"; +import { Graph } from "@/graph/Graph"; +import Graph3dPlugin from "@/main"; + +/** + * the files from the search result or the files from the search engine cannot be used directly + */ +export const getGraphAfterProcessingConfig = ( + plugin: Graph3dPlugin, + { + files, + graphType, + setting, + centerFile, + }: { + files: TAbstractFile[]; + graphType: GraphType; + setting: BaseFilterSettings | LocalFilterSetting; + centerFile?: TAbstractFile; + } +) => { + if (graphType === GraphType.local && "depth" in setting) { + // then it is a local graph + return Graph.createFromMask( + (node) => { + // if node is an orphan and show orphan is false, then we will not show it + if (node.links.length === 0 && !setting.showOrphans) return false; + // if node is not a markdown and show attachment is false, then we will not show it + if (!node.path.endsWith(".md") && !setting.showAttachments) return false; + // if the node is not in the files, then we will not show it + return files.length === 0 ? true : files.some((file) => file.path === node.path); + }, + // active file must exist in local graph + plugin.globalGraph.getLocalGraph({ + path: centerFile?.path ?? "", + depth: setting.depth, + linkType: setting.linkType, + }) + ); + } else { + // if it is global graph + return Graph.createFromMask((node) => { + // if node is an orphan and show orphan is false, then we will not show it + if (node.links.length === 0 && !setting.showOrphans) return false; + // if node is not a markdown and show attachment is false, then we will not show it + if (!node.path.endsWith(".md") && !setting.showAttachments) return false; + // if the node is not in the files, then we will not show it + return files.length === 0 ? true : files.some((file) => file.path === node.path); + }, plugin.globalGraph); + } +}; diff --git a/src/views/settings/categories/getMySwitcher.ts b/src/views/settings/categories/getMySwitcher.ts index 3e6965d..e0785e7 100644 --- a/src/views/settings/categories/getMySwitcher.ts +++ b/src/views/settings/categories/getMySwitcher.ts @@ -19,8 +19,7 @@ export const getMySwitcher = (view: NewGraph3dView) => { const suggestions = await super.getSuggestions(query); const allFilePaths = view .getForceGraph() - .getInstance() - .graphData() + .instance.graphData() .nodes.map((n) => n.path); return suggestions.filter(Boolean).filter((s) => { // only show files in this view