coderaph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * 3D Force-Directed Layout Engine
3
+ *
4
+ * Simulates a physical system where nodes repel each other,
5
+ * edges act as springs, and a centering force pulls everything
6
+ * toward the origin. Positions are mutated directly on the
7
+ * node objects passed to the constructor.
8
+ */
9
+
10
+ const DEFAULT_REPULSION = 500;
11
+ const DEFAULT_ATTRACTION = 0.02;
12
+ const DEFAULT_IDEAL_DISTANCE = 30;
13
+ const CENTER_STRENGTH = 0.01;
14
+ const DAMPING_FACTOR = 0.9;
15
+ const STABLE_ENERGY_THRESHOLD = 0.5;
16
+
17
+ export class ForceGraph {
18
+ /**
19
+ * @param {Array<{id: string}>} nodes - node objects, will get x,y,z,vx,vy,vz props added
20
+ * @param {Array<{source: string, target: string}>} edges
21
+ */
22
+ /**
23
+ * @param {Array<{id: string}>} nodes - node objects, will get x,y,z,vx,vy,vz props added
24
+ * @param {Array<{source: string, target: string}>} edges
25
+ * @param {boolean} [is2D=false] - if true, constrain layout to z=0 plane
26
+ */
27
+ constructor(nodes, edges, is2D = false) {
28
+ this.nodes = nodes;
29
+ this.edges = edges;
30
+ this.is2D = is2D;
31
+
32
+ // Build id -> node lookup
33
+ /** @type {Map<string, object>} */
34
+ this.nodeMap = new Map();
35
+ for (const node of nodes) {
36
+ this.nodeMap.set(node.id, node);
37
+ }
38
+
39
+ // Build adjacency map for O(1) edge access
40
+ // Each node id maps to an array of connected node ids
41
+ /** @type {Map<string, string[]>} */
42
+ this.adjacency = new Map();
43
+ for (const node of nodes) {
44
+ this.adjacency.set(node.id, []);
45
+ }
46
+ for (const edge of edges) {
47
+ this.adjacency.get(edge.source)?.push(edge.target);
48
+ this.adjacency.get(edge.target)?.push(edge.source);
49
+ }
50
+
51
+ // Pinned nodes: id -> {x, y, z}
52
+ /** @type {Map<string, {x: number, y: number, z: number}>} */
53
+ this.pinned = new Map();
54
+
55
+ // Tunable parameters
56
+ this.repulsion = DEFAULT_REPULSION;
57
+ this.attraction = DEFAULT_ATTRACTION;
58
+ this.idealDistance = DEFAULT_IDEAL_DISTANCE;
59
+
60
+ // Total kinetic energy from last tick
61
+ this._energy = Infinity;
62
+
63
+ // Initialize node positions on a sphere surface
64
+ this._initPositions();
65
+ }
66
+
67
+ /**
68
+ * Distribute nodes randomly on a sphere surface.
69
+ * Radius = sqrt(nodes.length) * 10
70
+ */
71
+ _initPositions() {
72
+ const radius = Math.sqrt(this.nodes.length) * 10;
73
+
74
+ for (const node of this.nodes) {
75
+ if (this.is2D) {
76
+ // Circle distribution on z=0 plane
77
+ const theta = Math.random() * 2 * Math.PI;
78
+ node.x = radius * Math.cos(theta);
79
+ node.y = radius * Math.sin(theta);
80
+ node.z = 0;
81
+ } else {
82
+ // Random point on sphere using spherical coordinates
83
+ const theta = Math.random() * 2 * Math.PI;
84
+ const phi = Math.acos(2 * Math.random() - 1);
85
+ node.x = radius * Math.sin(phi) * Math.cos(theta);
86
+ node.y = radius * Math.sin(phi) * Math.sin(theta);
87
+ node.z = radius * Math.cos(phi);
88
+ }
89
+ node.vx = 0;
90
+ node.vy = 0;
91
+ node.vz = 0;
92
+ }
93
+ }
94
+
95
+ /** Run one simulation step. Call this in requestAnimationFrame. */
96
+ tick() {
97
+ const nodes = this.nodes;
98
+ const len = nodes.length;
99
+
100
+ // 1. Repulsion (all pairs, O(n^2))
101
+ for (let i = 0; i < len; i++) {
102
+ const a = nodes[i];
103
+ for (let j = i + 1; j < len; j++) {
104
+ const b = nodes[j];
105
+ const dx = a.x - b.x;
106
+ const dy = a.y - b.y;
107
+ const dz = a.z - b.z;
108
+ const distSq = dx * dx + dy * dy + dz * dz + 0.01;
109
+ const force = this.repulsion / distSq;
110
+ const dist = Math.sqrt(distSq);
111
+ const fx = (dx / dist) * force;
112
+ const fy = (dy / dist) * force;
113
+ const fz = (dz / dist) * force;
114
+
115
+ a.vx += fx;
116
+ a.vy += fy;
117
+ a.vz += fz;
118
+ b.vx -= fx;
119
+ b.vy -= fy;
120
+ b.vz -= fz;
121
+ }
122
+ }
123
+
124
+ // 2. Attraction (connected pairs via edges)
125
+ for (const edge of this.edges) {
126
+ const a = this.nodeMap.get(edge.source);
127
+ const b = this.nodeMap.get(edge.target);
128
+ if (!a || !b) continue;
129
+
130
+ const dx = b.x - a.x;
131
+ const dy = b.y - a.y;
132
+ const dz = b.z - a.z;
133
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz + 0.01);
134
+ const force = (dist - this.idealDistance) * this.attraction;
135
+ const fx = (dx / dist) * force;
136
+ const fy = (dy / dist) * force;
137
+ const fz = (dz / dist) * force;
138
+
139
+ a.vx += fx;
140
+ a.vy += fy;
141
+ a.vz += fz;
142
+ b.vx -= fx;
143
+ b.vy -= fy;
144
+ b.vz -= fz;
145
+ }
146
+
147
+ // 3. Centering force + 4. Damping + 5. Position update
148
+ let energy = 0;
149
+
150
+ for (const node of nodes) {
151
+ // Centering
152
+ node.vx += -CENTER_STRENGTH * node.x;
153
+ node.vy += -CENTER_STRENGTH * node.y;
154
+ node.vz += -CENTER_STRENGTH * node.z;
155
+
156
+ // Damping
157
+ node.vx *= DAMPING_FACTOR;
158
+ node.vy *= DAMPING_FACTOR;
159
+ node.vz *= DAMPING_FACTOR;
160
+
161
+ // Handle pinned nodes
162
+ const pin = this.pinned.get(node.id);
163
+ if (pin) {
164
+ node.x = pin.x;
165
+ node.y = pin.y;
166
+ node.z = pin.z;
167
+ node.vx = 0;
168
+ node.vy = 0;
169
+ node.vz = 0;
170
+ } else {
171
+ // Position update
172
+ node.x += node.vx;
173
+ node.y += node.vy;
174
+ node.z += node.vz;
175
+ }
176
+
177
+ energy += node.vx * node.vx + node.vy * node.vy + node.vz * node.vz;
178
+ }
179
+
180
+ // 2D constraint: flatten z axis
181
+ if (this.is2D) {
182
+ for (const node of nodes) {
183
+ node.z = 0;
184
+ node.vz = 0;
185
+ }
186
+ }
187
+
188
+ this._energy = energy;
189
+ }
190
+
191
+ /** @returns {boolean} true if simulation has converged */
192
+ isStable() {
193
+ return this._energy < STABLE_ENERGY_THRESHOLD;
194
+ }
195
+
196
+ /** Restart simulation (e.g., after drag) */
197
+ reheat() {
198
+ for (const node of this.nodes) {
199
+ // Add random velocity to break out of local minima
200
+ node.vx += (Math.random() - 0.5) * 2;
201
+ node.vy += (Math.random() - 0.5) * 2;
202
+ node.vz += this.is2D ? 0 : (Math.random() - 0.5) * 2;
203
+ }
204
+ this._energy = Infinity;
205
+ }
206
+
207
+ /** Switch between 2D and 3D mode at runtime */
208
+ setIs2D(val) {
209
+ this.is2D = val;
210
+ if (val) {
211
+ for (const node of this.nodes) {
212
+ node.z = 0;
213
+ node.vz = 0;
214
+ }
215
+ }
216
+ this.reheat();
217
+ }
218
+
219
+ /**
220
+ * Pin a node at a fixed position.
221
+ * @param {string} id
222
+ * @param {number} x
223
+ * @param {number} y
224
+ * @param {number} z
225
+ */
226
+ pinNode(id, x, y, z) {
227
+ const node = this.nodeMap.get(id);
228
+ if (!node) return;
229
+ this.pinned.set(id, { x, y, z });
230
+ node.x = x;
231
+ node.y = y;
232
+ node.z = z;
233
+ node.vx = 0;
234
+ node.vy = 0;
235
+ node.vz = 0;
236
+ }
237
+
238
+ /**
239
+ * Unpin a node, allowing it to move freely again.
240
+ * @param {string} id
241
+ */
242
+ unpinNode(id) {
243
+ this.pinned.delete(id);
244
+ }
245
+ }
@@ -0,0 +1,93 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Coderaph</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="ui">
11
+ <div id="search-bar">
12
+ <input type="text" id="search-input" placeholder="Search symbols or files...">
13
+ <div id="search-nav" class="hidden">
14
+ <button id="search-prev">&lt;</button>
15
+ <span id="search-count"></span>
16
+ <button id="search-next">&gt;</button>
17
+ </div>
18
+ </div>
19
+ <div id="toolbar">
20
+ <button id="view-toggle" data-mode="file">File View</button>
21
+ <button id="settings-btn">Settings</button>
22
+ </div>
23
+ <div id="info-panel" class="hidden">
24
+ <h3 id="info-name"></h3>
25
+ <p id="info-kind"></p>
26
+ <p id="info-file"></p>
27
+ <button id="info-close">&times;</button>
28
+ </div>
29
+ <div id="settings-panel" class="hidden">
30
+ <h3>Settings <button id="settings-close">&times;</button></h3>
31
+ <label>Theme
32
+ <select id="setting-theme"><option value="dark" selected>Dark</option><option value="light">Light</option></select>
33
+ </label>
34
+ <label>Dimension
35
+ <select id="setting-dimension"><option value="3d" selected>3D</option><option value="2d">2D</option></select>
36
+ </label>
37
+ <label>Node Size <span id="val-node-size">1.0</span>
38
+ <input type="range" id="setting-node-size" min="0.3" max="3" step="0.1" value="1.0">
39
+ </label>
40
+ <label>Edge Thickness <span id="val-edge-thickness">1.0</span>
41
+ <input type="range" id="setting-edge-thickness" min="0.5" max="5" step="0.5" value="1.0">
42
+ </label>
43
+ <label>Node Distance <span id="val-node-distance">30</span>
44
+ <input type="range" id="setting-node-distance" min="10" max="100" step="5" value="30">
45
+ </label>
46
+ <label>Font Size <span id="val-font-size">10</span>px
47
+ <input type="range" id="setting-font-size" min="6" max="20" step="1" value="10">
48
+ </label>
49
+ <label>Show Labels
50
+ <input type="checkbox" id="setting-labels" checked>
51
+ </label>
52
+ <label>Show Arrows
53
+ <input type="checkbox" id="setting-arrows">
54
+ </label>
55
+ </div>
56
+ <div id="filter-panel">
57
+ <details open>
58
+ <summary>Filter</summary>
59
+ <div id="filter-kinds">
60
+ <label><input type="checkbox" data-kind="class" checked> Class</label>
61
+ <label><input type="checkbox" data-kind="function" checked> Function</label>
62
+ <label><input type="checkbox" data-kind="interface" checked> Interface</label>
63
+ <label><input type="checkbox" data-kind="type" checked> Type</label>
64
+ <label><input type="checkbox" data-kind="enum" checked> Enum</label>
65
+ <label><input type="checkbox" data-kind="variable" checked> Variable</label>
66
+ </div>
67
+ <input type="text" id="filter-path" placeholder="Filter by path...">
68
+ </details>
69
+ </div>
70
+ <div id="group-panel">
71
+ <details>
72
+ <summary>Group</summary>
73
+ <div id="group-list"></div>
74
+ <button id="group-add">새 그룹</button>
75
+ </details>
76
+ </div>
77
+ <div id="legend"></div>
78
+ </div>
79
+ <div id="tooltip" class="hidden"></div>
80
+ <div id="labels-container"></div>
81
+ <canvas id="canvas"></canvas>
82
+ <script type="importmap">
83
+ {
84
+ "imports": {
85
+ "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
86
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
87
+ }
88
+ }
89
+ </script>
90
+ <script type="module" src="app.js"></script>
91
+ <script type="module" src="controls.js"></script>
92
+ </body>
93
+ </html>
@@ -0,0 +1,102 @@
1
+ :root {
2
+ --bg: #0a0a0a;
3
+ --text: #e0e0e0;
4
+ --text-muted: #aaa;
5
+ --surface: #1a1a1a;
6
+ --border: #333;
7
+ --border-hover: #444;
8
+ --btn-bg: #222;
9
+ --btn-hover: #333;
10
+ --close-color: #888;
11
+ --close-hover: #e0e0e0;
12
+ }
13
+ [data-theme="light"] {
14
+ --bg: #f0f0f0;
15
+ --text: #1a1a1a;
16
+ --text-muted: #555;
17
+ --surface: #ffffff;
18
+ --border: #ccc;
19
+ --border-hover: #999;
20
+ --btn-bg: #e8e8e8;
21
+ --btn-hover: #d0d0d0;
22
+ --close-color: #888;
23
+ --close-hover: #333;
24
+ }
25
+ * { margin: 0; padding: 0; box-sizing: border-box; }
26
+ body { overflow: hidden; background: var(--bg); font-family: system-ui, sans-serif; color: var(--text); }
27
+ #canvas { display: block; width: 100vw; height: 100vh; }
28
+ #ui { position: fixed; top: 0; left: 0; z-index: 10; padding: 12px; pointer-events: none; }
29
+ #ui > * { pointer-events: auto; }
30
+ #search-bar { margin-bottom: 8px; }
31
+ #search-input { width: 280px; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); color: var(--text); font-size: 14px; outline: none; }
32
+ #search-input:focus { border-color: #4488ff; }
33
+ #search-bar { display: flex; align-items: center; gap: 6px; }
34
+ #search-nav { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-muted); white-space: nowrap; }
35
+ #search-nav.hidden { display: none; }
36
+ #search-nav button { padding: 2px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); cursor: pointer; font-size: 12px; line-height: 1.4; }
37
+ #search-nav button:hover { background: var(--btn-hover); }
38
+ #toolbar { margin-bottom: 8px; display: flex; gap: 6px; }
39
+ #toolbar button { padding: 6px 14px; border-radius: 4px; border: 1px solid var(--border-hover); background: var(--btn-bg); color: var(--text); cursor: pointer; font-size: 13px; }
40
+ #toolbar button:hover { background: var(--btn-hover); }
41
+ #info-panel { position: fixed; top: 12px; right: 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; min-width: 220px; pointer-events: auto; }
42
+ #info-panel.hidden { display: none; }
43
+ #info-panel h3 { margin-bottom: 8px; font-size: 15px; }
44
+ #info-panel p { font-size: 13px; color: var(--text-muted); margin-bottom: 4px; }
45
+ #info-close { position: absolute; top: 8px; right: 8px; background: none; border: none; color: var(--close-color); cursor: pointer; font-size: 18px; }
46
+ #info-close:hover { color: var(--close-hover); }
47
+ #legend { margin-top: 8px; font-size: 12px; line-height: 1.8; }
48
+
49
+ /* Filter panel */
50
+ #filter-panel { margin-top: 8px; }
51
+ #filter-panel details { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
52
+ #filter-panel summary { cursor: pointer; font-size: 13px; font-weight: 600; user-select: none; }
53
+ #filter-kinds { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 6px; }
54
+ #filter-kinds label { font-size: 12px; display: flex; align-items: center; gap: 4px; cursor: pointer; }
55
+ #filter-kinds input { accent-color: #4488ff; }
56
+ #filter-path { width: 100%; margin-top: 6px; padding: 5px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); font-size: 12px; outline: none; }
57
+ #filter-path:focus { border-color: #4488ff; }
58
+
59
+ /* Group panel */
60
+ #group-panel { margin-top: 8px; }
61
+ #group-panel details { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
62
+ #group-panel summary { cursor: pointer; font-size: 13px; font-weight: 600; user-select: none; }
63
+ .group-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
64
+ .group-query { flex: 1; padding: 5px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); font-size: 12px; outline: none; }
65
+ .group-query:focus { border-color: #4488ff; }
66
+ .group-query::placeholder { color: var(--text-muted); }
67
+ .group-color-input { position: absolute; visibility: hidden; }
68
+ .group-color { display: inline-block; width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0; border: 1px solid var(--border); cursor: pointer; }
69
+ .group-remove { background: none; border: none; color: var(--close-color); cursor: pointer; font-size: 16px; padding: 0 2px; }
70
+ .group-remove:hover { color: var(--close-hover); }
71
+ #group-add { width: 100%; margin-top: 6px; padding: 6px; border-radius: 4px; border: 1px solid var(--border); background: rgba(170,102,204,0.15); color: var(--text); cursor: pointer; font-size: 12px; }
72
+ #group-add:hover { background: rgba(170,102,204,0.25); }
73
+ .group-hint { position: absolute; z-index: 25; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
74
+ .group-hint div { padding: 3px 6px; cursor: pointer; border-radius: 3px; }
75
+ .group-hint div:hover { background: var(--btn-hover); }
76
+ .group-hint b { font-weight: 600; }
77
+
78
+ /* Autocomplete dropdown */
79
+ .autocomplete-list { position: absolute; z-index: 30; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; max-height: 180px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-size: 12px; min-width: 200px; }
80
+ .autocomplete-list.hidden { display: none; }
81
+ .autocomplete-item { padding: 5px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
82
+ .autocomplete-item:hover, .autocomplete-item.active { background: var(--btn-hover); }
83
+ .autocomplete-item .ac-match { color: #4488ff; font-weight: 600; }
84
+
85
+ /* Tooltip */
86
+ #tooltip { position: fixed; z-index: 20; padding: 6px 10px; border-radius: 4px; background: var(--surface); border: 1px solid var(--border); color: var(--text); font-size: 12px; pointer-events: none; white-space: nowrap; line-height: 1.5; }
87
+ #tooltip.hidden { display: none; }
88
+
89
+ /* Settings panel */
90
+ #settings-panel { position: fixed; top: 50px; left: 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; min-width: 240px; pointer-events: auto; z-index: 15; }
91
+ #settings-panel.hidden { display: none; }
92
+ #settings-panel h3 { margin-bottom: 12px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; }
93
+ #settings-panel label { display: flex; justify-content: space-between; align-items: center; font-size: 13px; margin-bottom: 8px; gap: 8px; }
94
+ #settings-panel input[type="range"] { width: 110px; accent-color: #4488ff; }
95
+ #settings-panel select { padding: 3px 6px; border-radius: 4px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); font-size: 12px; }
96
+ #settings-panel input[type="checkbox"] { accent-color: #4488ff; }
97
+ #settings-close { background: none; border: none; color: var(--close-color); cursor: pointer; font-size: 16px; }
98
+ #settings-close:hover { color: var(--close-hover); }
99
+
100
+ /* Node labels */
101
+ #labels-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; overflow: hidden; }
102
+ .node-label { position: absolute; font-size: 10px; color: var(--text); white-space: nowrap; transform: translate(-50%, -120%); text-shadow: 0 0 3px var(--bg), 0 0 3px var(--bg); }