diff options
-rw-r--r-- | components/config.ts | 5 | ||||
-rw-r--r-- | org-roam-ui.el | 65 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | pages/index.tsx | 119 | ||||
-rw-r--r-- | yarn.lock | 5 |
5 files changed, 150 insertions, 45 deletions
diff --git a/components/config.ts b/components/config.ts index 2c7a1b6..9c49068 100644 --- a/components/config.ts +++ b/components/config.ts @@ -90,3 +90,8 @@ export const initialVisuals = { linkHighlight: '', backgroundColor: 'white', } + +export const initialBehavior = { + follow: 'Zoom', + followLocalOrZoom: true, +} diff --git a/org-roam-ui.el b/org-roam-ui.el index 8a3a5b3..877a4d6 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -86,6 +86,8 @@ E.g. '((bg . '#1E2029') :type 'list) (defvar org-roam-ui--ws-current-node nil) +(defvar oru-ws nil + "The websocket for org-roam-ui.") ;;;###autoload (define-minor-mode @@ -105,22 +107,24 @@ This serves the web-build and API over HTTP." (websocket-server 35903 :host 'local - :on-open (lambda (ws) (progn (setq oru-ws ws) (org-roam-ui--send-graphdata) (org-roam-ui-sync-theme--advice) (message "Connection established with org-roam-ui"))) - :on-close (lambda (_websocket) (setq oru-ws nil) (message "Connection with org-roam-ui closed succesfully.")))) + :on-open (lambda (ws) (progn (setq oru-ws + ws) (org-roam-ui--send-graphdata) (message "Connection established with org-roam-ui") + (add-hook 'post-command-hook #'org-roam-ui--update-current-node))) + :on-close (lambda (_websocket) (setq oru-ws + nil) (message "Connection with org-roam-ui closed succesfully.")))) (if (boundp 'counsel-load-theme) (advice-add 'counsel-load-theme :after #'org-roam-ui-sync-theme--advice) - (advice-add 'load-theme :after #'org-roam-ui-sync-theme-manually)) - (add-hook 'post-command-hook #'org-roam-ui--update-current-node) - (add-hook 'post-command-hook #'org-roam-ui-update)) + (advice-add 'load-theme :around #'org-roam-ui-sync-theme--advice)) + (add-hook 'post-command-hook #'org-roam-ui--update-current-node)) + ;(add-hook 'post-command-hook #'org-roam-ui-update)) (t (progn + (websocket-server-close org-roam-ui-ws) (remove-hook 'post-command-hook #'org-roam-ui-update) (remove-hook 'post-command-hook #'org-roam-ui--update-current-node) (if (boundp 'counsel-load-theme) (advice-remove 'counsel-load-theme #'org-roam-ui-sync-theme--advice) (advice-remove 'load-theme #'org-roam-ui-sync-theme--advice)) - (websocket-server-close org-roam-ui-ws) - (delete-process org-roam-ui-ws) (httpd-stop))))) @@ -135,11 +139,13 @@ This serves the web-build and API over HTTP." (websocket-send-text oru-ws (json-encode `((type . "graphdata") (data . ,response)))))) (defun org-roam-ui--update-current-node () + (when (websocket-openp oru-ws) (let* ((node (org-roam-id-at-point))) (unless (string-match-p (regexp-quote "Minibuf") (buffer-name (current-buffer))) (unless (string= org-roam-ui--ws-current-node node) (setq org-roam-ui--ws-current-node node) - (websocket-send-text oru-ws (json-encode `((type . "command") (data . ((commandName . "follow") (id . ,node)))))))))) + (websocket-send-text oru-ws (json-encode `((type . "command") (data +. ((commandName . "follow") (id . ,node))))))))))) (defun org-roam-ui-show-node () "Open the current org-roam node in org-roam-ui." @@ -148,6 +154,7 @@ This serves the web-build and API over HTTP." (defun org-roam-ui-sync-theme--advice () "Function which is called after load-theme to sync your current theme with org-roam-ui." + (message "Syncing theme") (websocket-send-text oru-ws (json-encode `((type . "theme") (data . ,(org-roam-ui--update-theme)))))) (defun org-roam-ui-sync-theme-manually () @@ -245,19 +252,35 @@ This function is added to `post-command-hook'." )) -;; (defservlet* theme text/stream () -;; (progn) -;; (if org-roam-ui-sync-theme -;; (if (boundp 'doom-themes--colors) -;; (let* -;; ((colors (butlast doom-themes--colors (- (length doom-themes--colors) 25))) ui-theme (list nil)) -;; (progn -;; (dolist (color colors) (push (cons (car color) (car (cdr color))) ui-theme)) -;; ui-theme)) -;; (insert (format "data: %s\n\n" (json-encode (org-roam-ui-get-theme))))) -;; (when org-roam-ui-custom-theme -;; (insert (format "data %s\n\n" (json-encode org-roam-ui-custom-theme))))) -;; (httpd-send-header t "text/event-stream" 200 :Access-Control-Allow-Origin "*")) +;;;; commands +(defun orui-zoom-to-node (&optional id speed padding) + "Move the view of the graph to the node at points, or optionally a node of your choosing. +Optionally takes three arguments: +The id of the node you want to travel to. +The time in ms it takes to make the transition. +The padding around the nodes in the viewport." + (interactive) + (if-let ((node (or id (org-roam-id-at-point)))) + (websocket-send-text oru-ws (json-encode `((type . "command") (data . + ((commandName . "zoom") (id . ,node) (speed . ,speed) (padding . ,padding))))))) + (message "No node found.")) + +(defun orui-toggle-following () + "Set whether ORUI should follow your every move in emacs. Default yes." + (interactive) + (if (member #'org-roam-ui--update-current-node post-command-hook) + (progn + (remove-hook 'post-command-hook #'org-roam-ui--update-current-node) + (message "Org-Roam-UI will now leave you alone.") + (add-hook 'post-command-hook #'org-roam-ui--update-current-node) + (message "Org-Roam-UI will now follow you around.")) + )) + +(defun orui-toggle-local-zoom () + "Toggles whether org-roam-ui should go to the local view of a given node or zoom to it. +Defaults to local." + (interactive) + (org-roam-ui--send-command "toggle" `(id . yes))) (provide 'org-roam-ui) ;;; org-roam-ui.el ends here diff --git a/package.json b/package.json index 55b4c5e..7862a9b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-dom": "17.0.2", "react-force-graph": "^1.41.7", "react-spring": "^9.2.4", + "reconnecting-websocket": "^4.4.0", "three-spritetext": "^1.6.2", "use-constant": "^1.1.0" }, diff --git a/pages/index.tsx b/pages/index.tsx index e0e8b6e..3540a4c 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -22,10 +22,19 @@ import { useAnimation } from '@lilib/hooks' import { Box, useTheme } from '@chakra-ui/react' -import { initialPhysics, initialFilter, initialVisuals } from '../components/config' +import { + initialPhysics, + initialFilter, + initialVisuals, + initialBehavior, +} from '../components/config' import { Tweaks } from '../components/tweaks' import { ThemeContext, ThemeContextProps } from './themecontext' +import SpriteText from 'three-spritetext' + +import ReconnectingWebSocket from 'reconnecting-websocket' + // react-force-graph fails on import when server-rendered // https://github.com/vasturiano/react-force-graph/issues/155 const ForceGraph2D = ( @@ -62,6 +71,7 @@ export function GraphPage() { const [visuals, setVisuals] = usePersistantState('visuals', initialVisuals) const [graphData, setGraphData] = useState<GraphData | null>(null) const [emacsNodeId, setEmacsNodeId] = useState<string | null>(null) + const [behavior, setBehavior] = usePersistantState('behavior', initialBehavior) const nodeByIdRef = useRef<NodeById>({}) const linksByNodeIdRef = useRef<LinksByNodeId>({}) @@ -113,10 +123,23 @@ export function GraphPage() { const orgRoamGraphDataClone = JSON.parse(JSON.stringify(orgRoamGraphDataWithFileLinks)) setGraphData(orgRoamGraphDataClone) } + const { setEmacsTheme } = useContext(ThemeContext) + + const [threeDim, setThreeDim] = useState(false) + + const graph2dRef = useRef<any>(null) + const graph3dRef = useRef<any>(null) + useEffect(() => { +<<<<<<< HEAD + const fg = threeDim ? graph3dRef.current : graph2dRef.current + const socket = new ReconnectingWebSocket('ws://localhost:35903') + socket.addEventListener('open', (event) => { +======= const socket = new WebSocket('ws://localhost:35903') socket.addEventListener('open', () => { +>>>>>>> 4e3d884c402b7dc7d12f0cae88a9a312b10f166f console.log('Connection with Emacs established') }) socket.addEventListener('message', (event) => { @@ -131,14 +154,18 @@ export function GraphPage() { case 'follow': return setEmacsNodeId(message.data.id) case 'zoom': { + console.log(message) const links = linksByNodeIdRef.current[message.data.id!] ?? [] const nodes = Object.fromEntries( [ - message.commandData.id! as string, + message.data.id! as string, ...links.flatMap((link) => [link.source, link.target]), ].map((nodeId) => [nodeId, {}]), ) - /* zoomToFit(500, 200, (node: OrgRoamNode)=>nodes[node.id!]) */ + fg.zoomToFit(2000, 200, (node: OrgRoamNode) => nodes[node.id!]) + } + case 'toggle': { + /* setBehavior({ ...behavior, followLocalorZoom: !behavior.followLocalOrZoom }) */ } default: return console.error('unknown message type', message.type) @@ -147,7 +174,16 @@ export function GraphPage() { }) }, []) +<<<<<<< HEAD + useEffect(() => { + if (!emacsNodeId) { + return + } + //fetchGraphData() + }, [emacsNodeId]) +======= const [threeDim, setThreeDim] = useState(false) +>>>>>>> 4e3d884c402b7dc7d12f0cae88a9a312b10f166f if (!graphData) { return null @@ -178,6 +214,9 @@ export function GraphPage() { emacsNodeId, filter, visuals, + behavior, + graph2dRef, + graph3dRef, }} /> </Box> @@ -194,14 +233,25 @@ export interface GraphProps { filter: typeof initialFilter emacsNodeId: string | null visuals: typeof initialVisuals + behavior: typeof initialBehavior + graph2dRef: any + graph3dRef: any } export const Graph = function (props: GraphProps) { - const { physics, graphData, threeDim, linksByNodeId, filter, emacsNodeId, nodeById, visuals } = - props - - const graph2dRef = useRef<any>(null) - const graph3dRef = useRef<any>(null) + const { + physics, + graphData, + threeDim, + linksByNodeId, + filter, + emacsNodeId, + nodeById, + visuals, + behavior, + graph2dRef, + graph3dRef, + } = props // react-force-graph does not track window size // https://github.com/vasturiano/react-force-graph/issues/233 @@ -215,33 +265,43 @@ export const Graph = function (props: GraphProps) { if (!emacsNodeId) { return } - switch (physics.follow) { - case 'Local': - setScope({ nodeIds: [emacsNodeId] }) - break - case 'Zoom': - default: + const fg = threeDim ? graph3dRef.current : graph2dRef.current + if (behavior.followLocalOrZoom) { + setScope({ nodeIds: [emacsNodeId] }) + } else { + fg?.zoomToFit(1000, 200, (node: NodeObject) => getNeighborNodes(emacsNodeId)[node.id!]) + setHoverNode(nodeById[emacsNodeId] as NodeObject) } }, [emacsNodeId]) - const centralHighlightedNode = hoverNode + const getNeighborNodes = (id: string) => { + const links = linksByNodeId[id]! ?? [] + return Object.fromEntries( + [id as string, ...links.flatMap((link) => [link.source, link.target])].map((nodeId) => [ + nodeId, + {}, + ]), + ) + } + const centralHighlightedNode = useRef<NodeObject | null>(null) + centralHighlightedNode.current = hoverNode const highlightedNodes = useMemo(() => { - if (!centralHighlightedNode) { + if (!centralHighlightedNode.current) { return {} } - const links = linksByNodeId[centralHighlightedNode.id!] + const links = linksByNodeId[centralHighlightedNode.current.id!] if (!links) { return {} } return Object.fromEntries( [ - centralHighlightedNode.id! as string, + centralHighlightedNode.current.id! as string, ...links.flatMap((link) => [link.source, link.target]), ].map((nodeId) => [nodeId, {}]), ) - }, [centralHighlightedNode, linksByNodeId]) + }, [centralHighlightedNode.current, linksByNodeId]) const filteredNodes = useMemo(() => { return graphData.nodes.filter((node) => { @@ -590,13 +650,13 @@ export const Graph = function (props: GraphProps) { linkColor: (link) => { const sourceId = typeof link.source === 'object' ? link.source.id! : (link.source as string) const targetId = typeof link.target === 'object' ? link.target.id! : (link.target as string) - const linkIsHighlighted = isLinkRelatedToNode(link, centralHighlightedNode) + const linkIsHighlighted = isLinkRelatedToNode(link, centralHighlightedNode.current) const linkWasHighlighted = isLinkRelatedToNode(link, lastHoverNode.current) const needsHighlighting = linkIsHighlighted || linkWasHighlighted return getLinkColor(sourceId as string, targetId as string, needsHighlighting) }, linkWidth: (link) => { - const linkIsHighlighted = isLinkRelatedToNode(link, centralHighlightedNode) + const linkIsHighlighted = isLinkRelatedToNode(link, centralHighlightedNode.current) const linkWasHighlighted = isLinkRelatedToNode(link, lastHoverNode.current) return linkIsHighlighted || linkWasHighlighted @@ -647,6 +707,18 @@ export const Graph = function (props: GraphProps) { nodeOpacity={physics.nodeOpacity} nodeResolution={physics.nodeResolution} linkOpacity={physics.linkOpacity} + nodeThreeObject={(node: OrgRoamNode) => { + if (!physics.labels) { + return + } + if (physics.labels === 1 && !highlightedNodes[node.id!]) { + return + } + const sprite = new SpriteText(node.title.substring(0, 30)) + sprite.color = '#ffffff' + sprite.textHeight = 8 + return sprite + }} /> ) : ( <ForceGraph2D ref={graph2dRef} {...graphCommonProps} /> @@ -655,10 +727,9 @@ export const Graph = function (props: GraphProps) { ) } -function isLinkRelatedToNode(link: LinkObject, centralHighlightedNode: NodeObject | null) { +function isLinkRelatedToNode(link: LinkObject, node: NodeObject | null) { return ( - (link.source as NodeObject).id! === centralHighlightedNode?.id! || - (link.target as NodeObject).id! === centralHighlightedNode?.id! + (link.source as NodeObject).id! === node?.id! || (link.target as NodeObject).id! === node?.id! ) } @@ -4299,6 +4299,11 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz" |