diff --git a/deno.jsonc b/deno.jsonc
index 2c6b547..e0b5eaa 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -1,5 +1,5 @@
{
- "exclude": ["doc/assets", "render/assets", "_site", "__snapshots__"],
+ "exclude": ["render/assets", "_site", "__snapshots__"],
"tasks": {
"lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A -",
"build": "deno task lume",
diff --git a/doc/assets/demo.js b/doc/assets/demo.js
index 9c51060..c5e5391 100644
--- a/doc/assets/demo.js
+++ b/doc/assets/demo.js
@@ -4,7 +4,6 @@
///
import ForceGraph from "https://esm.sh/force-graph@1.43.4"
-import { forceCluster } from "https://esm.sh/d3-force-cluster"
// import ForceGraph2D from "https://esm.sh/react-force-graph-2d@1.25.4"
// import ReactDom from "https://esm.sh/react-dom@17"
@@ -18,8 +17,19 @@ const graphDom = document.querySelector("div#graph")
if (!graphDom) throw new Error("graph dom not found")
const data = await fetch("./assets/data.json").then((res) => res.json())
+const imports = Object.fromEntries(
+ Object.entries(Object.groupBy(data.imports, (x) => x.source))
+ .map((
+ [k, v],
+ ) => [k, v.map((x) => data.nodes.find((node) => node.id === x.target))]),
+)
+let hoveredNode
+
+// console.log(imports)
// ReactDOM.render()
+const ARROW_WH_RATIO = 1.6
+const ARROW_VLEN_RATIO = 0.2
const graph = ForceGraph()(graphDom)
.width(graphDom.clientWidth)
@@ -29,9 +39,15 @@ const graph = ForceGraph()(graphDom)
.nodeAutoColorBy("path")
.linkAutoColorBy("color")
.nodeLabel("url")
- .linkDirectionalArrowLength((link) => link.type === "import" ? 5 : 2)
- .linkWidth((link) => link.type === "import" ? 3 : 0.5)
+ .linkDirectionalArrowLength(3)
+ .linkWidth(0.5)
.nodeCanvasObject((node, ctx, globalScale) => {
+ ctx.globalAlpha = hoveredNode
+ ? ((hoveredNode === node || imports[hoveredNode.id]?.includes(node))
+ ? 1
+ : 0.1)
+ : 1
+
const label = /**@type{string}*/ (node.name)
const fontSize = (node.type === "import" ? 20 : 16) / globalScale
ctx.font = `${fontSize}px Sans-Serif`
@@ -80,6 +96,97 @@ const graph = ForceGraph()(graphDom)
node.bgWidth = bgWidth
// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgHeight = bgHeight
+
+ // if (hoveredNode === node) {
+ {
+ ctx.save()
+ ctx.globalAlpha = (hoveredNode === node) ? 1 : 0.25
+ ctx.lineWidth = (hoveredNode === node) ? 3 : 1
+ const arrowLength = (hoveredNode === node) ? 24 : 6
+ const arrowRelPos = 0.5
+ const arrowColor = node.color // "rgba(241, 21, 21, 0.521)"
+ const arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2
+
+ imports[node.id]?.forEach((target) => {
+ // draws line
+ ctx.beginPath()
+ ctx.moveTo(
+ // @ts-ignore: node do has x and y but force-graph marks it optional
+ node.x,
+ // @ts-ignore: node do has x and y but force-graph marks it optional
+ node.y,
+ )
+ ctx.lineTo(
+ // @ts-ignore: node do has x and y but force-graph marks it optional
+ target.x,
+ // @ts-ignore: node do has x and y but force-graph marks it optional
+ target.y,
+ )
+ ctx.strokeStyle = arrowColor
+ ctx.stroke()
+
+ // draws arrow
+
+ const start = node
+ const end = target
+
+ if (
+ !start || !end || !start.hasOwnProperty("x") ||
+ !end.hasOwnProperty("x")
+ ) return // skip invalid link
+
+ // Construct bezier for curved lines
+ // const bzLine = link.__controlPoints &&
+ // new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y)
+
+ const getCoordsAlongLine =
+ // bzLine
+ // ? (t) => bzLine.get(t) // get position along bezier line
+ // :
+ (t) => ({ // straight line: interpolate linearly
+ x: start.x + (end.x - start.x) * t || 0,
+ y: start.y + (end.y - start.y) * t || 0,
+ })
+
+ const lineLen =
+ // bzLine
+ // ? bzLine.length()
+ // :
+ Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2))
+
+ const posAlongLine = 1 + arrowLength +
+ (lineLen - 1 - 1 - arrowLength) * arrowRelPos
+
+ const arrowHead = getCoordsAlongLine(posAlongLine / lineLen)
+ const arrowTail = getCoordsAlongLine(
+ (posAlongLine - arrowLength) / lineLen,
+ )
+ const arrowTailVertex = getCoordsAlongLine(
+ (posAlongLine - arrowLength * (1 - ARROW_VLEN_RATIO)) / lineLen,
+ )
+
+ const arrowTailAngle =
+ Math.atan2(arrowHead.y - arrowTail.y, arrowHead.x - arrowTail.x) -
+ Math.PI / 2
+
+ ctx.beginPath()
+
+ ctx.moveTo(arrowHead.x, arrowHead.y)
+ ctx.lineTo(
+ arrowTail.x + arrowHalfWidth * Math.cos(arrowTailAngle),
+ arrowTail.y + arrowHalfWidth * Math.sin(arrowTailAngle),
+ )
+ ctx.lineTo(arrowTailVertex.x, arrowTailVertex.y)
+ ctx.lineTo(
+ arrowTail.x - arrowHalfWidth * Math.cos(arrowTailAngle),
+ arrowTail.y - arrowHalfWidth * Math.sin(arrowTailAngle),
+ )
+
+ ctx.fillStyle = arrowColor
+ ctx.fill()
+ })
+ ctx.restore()
+ }
})
.nodePointerAreaPaint((node, color, ctx) => {
ctx.fillStyle = color
@@ -96,7 +203,12 @@ const graph = ForceGraph()(graphDom)
})
// @ts-ignore: node has url but force-graph lacks generics to know it
.onNodeClick((node) => globalThis.open(node.url))
+ .onNodeHover((node) => {
+ hoveredNode = node
+ })
+ .autoPauseRedraw(false)
+graph.d3Force("link")?.distance(90)
// graph.d3Force("link")?.strength(link => {
// console.log(link)
// return link.type === "import" ? 1 : 0
diff --git a/doc/index.page.ts b/doc/index.page.ts
index 775c831..ea73d3b 100644
--- a/doc/index.page.ts
+++ b/doc/index.page.ts
@@ -7,7 +7,7 @@ const header = /*md*/ `
-(StackGraph 저장소의 모든 변수 관계도, 붉은 선은 의존 관계, 회색 선은 디렉터리/파일 트리)
+(StackGraph 저장소의 모든 변수 관계도, 유색 선은 의존 관계, 무색 선은 디렉터리/파일 트리)
`
diff --git a/doc/main.ts b/doc/main.ts
index bb7999b..68576c3 100644
--- a/doc/main.ts
+++ b/doc/main.ts
@@ -23,7 +23,7 @@ export type Link = {
color: string
type: Type
}
-type RawNode = Pick
+type RawNode = Pick
export type Node = {
id: string
name: string
@@ -32,11 +32,11 @@ export type Node = {
path: string
dir: string
url: string
- line?: string
+ line: string
type: Type
}
-const linkNode = (
+const linkNode = (
node: T,
) => {
const dir = dirname(node.path)
@@ -63,7 +63,7 @@ if (import.meta.main) {
const root = import.meta.dirname + "/../"
const files = project.addSourceFilesAtPaths(
- import.meta.dirname + "/../**/*.ts",
+ import.meta.dirname + "/../graph/**/*_test.ts",
)
const decls = Stream.fromObjectValues(files).flatMap(getAllDecls).toArray()
@@ -114,6 +114,7 @@ if (import.meta.main) {
id: path,
name: path,
path,
+ line: "",
type: "path" as const,
color: "#bdbbbb48",
textColor: "#00000080",
@@ -126,12 +127,15 @@ if (import.meta.main) {
import.meta.dirname + "/assets/data.json",
JSON.stringify(
{
- links: Stream.from(links, dirLinks).toArray(),
+ links: dirLinks,
+ imports: links,
+ // links: Stream.from(links, dirLinks).toArray(),
nodes: [
...nodes.map(linkNode).map(colorNode),
...dirNodes.map(linkNode),
],
- } satisfies { links: Link[]; nodes: Node[] },
+ },
+ // satisfies { links: Link[]; nodes: Node[] },
null,
2,
),