synabun 1.3.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.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — 2D Orbital Layout
|
|
3
|
+
// Computes hierarchical orbital positions:
|
|
4
|
+
// Parent categories in a ring → child categories orbit parents →
|
|
5
|
+
// memory cards orbit their category in concentric rings
|
|
6
|
+
// Two-pass layout: compute radii first, then place with proper spacing
|
|
7
|
+
// ═══════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
import { gfx } from './gfx.js';
|
|
10
|
+
import { state } from '../../shared/state.js';
|
|
11
|
+
import { KEYS } from '../../shared/constants.js';
|
|
12
|
+
import { storage } from '../../shared/storage.js';
|
|
13
|
+
|
|
14
|
+
// ═══════════════════════════════════════════
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
// ═══════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
export const CARD_W = 180;
|
|
19
|
+
export const CARD_H = 80;
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════
|
|
22
|
+
// PUBLIC API
|
|
23
|
+
// ═══════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute orbital layout for all visible nodes.
|
|
27
|
+
* Uses a two-pass approach: first compute radii, then re-place with proper spacing.
|
|
28
|
+
* @param {Array} nodes state.allNodes (filtered by activeCategories)
|
|
29
|
+
* @param {Object} categoryMetadata state.categoryMetadata
|
|
30
|
+
* @param {Set} activeCategories state.activeCategories
|
|
31
|
+
* @returns {{ regions: CategoryRegion[], cards: CardPosition[] }}
|
|
32
|
+
*/
|
|
33
|
+
export function computeLayout(nodes, categoryMetadata, activeCategories) {
|
|
34
|
+
// ── 1. Group nodes by category hierarchy ──
|
|
35
|
+
const tree = _buildCategoryTree(nodes, categoryMetadata, activeCategories);
|
|
36
|
+
|
|
37
|
+
// ── 2. Pass 1: Temporary placement + compute radii ──
|
|
38
|
+
_layoutParents(tree);
|
|
39
|
+
for (const parent of tree) {
|
|
40
|
+
_layoutChildren(parent);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Layout cards (this sets region.radius for each region)
|
|
44
|
+
const allCards = [];
|
|
45
|
+
for (const parent of tree) {
|
|
46
|
+
if (parent.memories.length > 0) {
|
|
47
|
+
const cards = _layoutCards(parent, parent.memories, true);
|
|
48
|
+
allCards.push(...cards);
|
|
49
|
+
}
|
|
50
|
+
for (const child of parent.children) {
|
|
51
|
+
if (child.memories.length > 0) {
|
|
52
|
+
const cards = _layoutCards(child, child.memories, false);
|
|
53
|
+
allCards.push(...cards);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 3. Pass 2: Re-position using actual radii ──
|
|
59
|
+
// Snapshot old positions before re-layout
|
|
60
|
+
const oldPositions = new Map();
|
|
61
|
+
for (const parent of tree) {
|
|
62
|
+
oldPositions.set(parent.name, { cx: parent.cx, cy: parent.cy });
|
|
63
|
+
for (const child of parent.children) {
|
|
64
|
+
oldPositions.set(child.name, { cx: child.cx, cy: child.cy });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Re-position children with actual radii (still orbiting old parent positions)
|
|
69
|
+
for (const parent of tree) {
|
|
70
|
+
_reLayoutChildren(parent);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Re-position parents in a properly-spaced ring using actual cluster radii
|
|
74
|
+
_reLayoutParentsWithRadii(tree);
|
|
75
|
+
|
|
76
|
+
// Cascade parent ring movement to children — they must orbit the NEW parent center
|
|
77
|
+
for (const parent of tree) {
|
|
78
|
+
const old = oldPositions.get(parent.name);
|
|
79
|
+
if (!old) continue;
|
|
80
|
+
const dx = parent.cx - old.cx;
|
|
81
|
+
const dy = parent.cy - old.cy;
|
|
82
|
+
if (dx === 0 && dy === 0) continue;
|
|
83
|
+
for (const child of parent.children) {
|
|
84
|
+
child.cx += dx;
|
|
85
|
+
child.cy += dy;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Propagate position deltas to cards
|
|
90
|
+
_propagateDeltas(tree, allCards, oldPositions);
|
|
91
|
+
|
|
92
|
+
// ── 4. Collision avoidance ──
|
|
93
|
+
const preCollision = new Map();
|
|
94
|
+
const allRegions = flattenRegions(tree);
|
|
95
|
+
for (const r of allRegions) {
|
|
96
|
+
preCollision.set(r.name, { cx: r.cx, cy: r.cy });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_resolveCollisions(tree);
|
|
100
|
+
|
|
101
|
+
// Propagate collision deltas to cards
|
|
102
|
+
_propagateDeltas(tree, allCards, preCollision);
|
|
103
|
+
|
|
104
|
+
// ── 5. Restore saved positions (overrides computed ones) ──
|
|
105
|
+
_restoreSavedPositions(tree, allCards);
|
|
106
|
+
|
|
107
|
+
return { regions: tree, cards: allCards };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear all saved positions and force a fresh orbital layout on next compute.
|
|
112
|
+
*/
|
|
113
|
+
export function resetLayout() {
|
|
114
|
+
try { storage.removeItem(KEYS.NODE_POS_2D); } catch (_) {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the flat list of all regions (parents + children) from a tree.
|
|
119
|
+
* @param {Array} tree The top-level regions from computeLayout
|
|
120
|
+
* @returns {Array}
|
|
121
|
+
*/
|
|
122
|
+
export function flattenRegions(tree) {
|
|
123
|
+
const flat = [];
|
|
124
|
+
for (const parent of tree) {
|
|
125
|
+
flat.push(parent);
|
|
126
|
+
for (const child of parent.children) {
|
|
127
|
+
flat.push(child);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return flat;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// ═══════════════════════════════════════════
|
|
135
|
+
// INTERNAL: Build category tree
|
|
136
|
+
// ═══════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
function _buildCategoryTree(nodes, categoryMetadata, activeCategories) {
|
|
139
|
+
// Map: parentName → { name, cx, cy, radius, color, children[], memories[], isParent }
|
|
140
|
+
const parentMap = new Map();
|
|
141
|
+
const childMap = new Map(); // childName → { name, cx, cy, radius, color, memories[], parentName }
|
|
142
|
+
|
|
143
|
+
// Discover parent/child structure from metadata
|
|
144
|
+
for (const [name, meta] of Object.entries(categoryMetadata)) {
|
|
145
|
+
if (!activeCategories.has(name)) continue;
|
|
146
|
+
|
|
147
|
+
if (meta.is_parent) {
|
|
148
|
+
if (!parentMap.has(name)) {
|
|
149
|
+
parentMap.set(name, {
|
|
150
|
+
name,
|
|
151
|
+
cx: 0, cy: 0,
|
|
152
|
+
radius: 0,
|
|
153
|
+
color: meta.color || '#888',
|
|
154
|
+
logoUrl: meta.logo || null,
|
|
155
|
+
description: meta.description || '',
|
|
156
|
+
children: [],
|
|
157
|
+
memories: [],
|
|
158
|
+
isParent: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} else if (meta.parent) {
|
|
162
|
+
// Ensure parent exists
|
|
163
|
+
if (!parentMap.has(meta.parent)) {
|
|
164
|
+
const parentMeta = categoryMetadata[meta.parent] || {};
|
|
165
|
+
parentMap.set(meta.parent, {
|
|
166
|
+
name: meta.parent,
|
|
167
|
+
cx: 0, cy: 0,
|
|
168
|
+
radius: 0,
|
|
169
|
+
color: parentMeta.color || '#888',
|
|
170
|
+
logoUrl: parentMeta.logo || null,
|
|
171
|
+
description: parentMeta.description || '',
|
|
172
|
+
children: [],
|
|
173
|
+
memories: [],
|
|
174
|
+
isParent: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const child = {
|
|
178
|
+
name,
|
|
179
|
+
cx: 0, cy: 0,
|
|
180
|
+
radius: 0,
|
|
181
|
+
color: meta.color || '#888',
|
|
182
|
+
logoUrl: meta.logo || null,
|
|
183
|
+
description: meta.description || '',
|
|
184
|
+
memories: [],
|
|
185
|
+
parentName: meta.parent,
|
|
186
|
+
isParent: false,
|
|
187
|
+
};
|
|
188
|
+
childMap.set(name, child);
|
|
189
|
+
parentMap.get(meta.parent).children.push(child);
|
|
190
|
+
} else {
|
|
191
|
+
// Standalone category (no parent, not is_parent) — treat as its own parent
|
|
192
|
+
if (!parentMap.has(name)) {
|
|
193
|
+
parentMap.set(name, {
|
|
194
|
+
name,
|
|
195
|
+
cx: 0, cy: 0,
|
|
196
|
+
radius: 0,
|
|
197
|
+
color: meta.color || '#888',
|
|
198
|
+
logoUrl: meta.logo || null,
|
|
199
|
+
description: meta.description || '',
|
|
200
|
+
children: [],
|
|
201
|
+
memories: [],
|
|
202
|
+
isParent: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Assign each node to its category
|
|
209
|
+
for (const node of nodes) {
|
|
210
|
+
const cat = node.payload?.category;
|
|
211
|
+
if (!cat || !activeCategories.has(cat)) continue;
|
|
212
|
+
|
|
213
|
+
if (childMap.has(cat)) {
|
|
214
|
+
childMap.get(cat).memories.push(node);
|
|
215
|
+
} else if (parentMap.has(cat)) {
|
|
216
|
+
parentMap.get(cat).memories.push(node);
|
|
217
|
+
} else {
|
|
218
|
+
// Unknown category — create standalone parent
|
|
219
|
+
const meta = categoryMetadata[cat] || {};
|
|
220
|
+
const parent = {
|
|
221
|
+
name: cat,
|
|
222
|
+
cx: 0, cy: 0,
|
|
223
|
+
radius: 0,
|
|
224
|
+
color: meta.color || '#888',
|
|
225
|
+
logoUrl: meta.logo || null,
|
|
226
|
+
description: meta.description || '',
|
|
227
|
+
children: [],
|
|
228
|
+
memories: [node],
|
|
229
|
+
isParent: true,
|
|
230
|
+
};
|
|
231
|
+
parentMap.set(cat, parent);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Filter out empty parents (no memories anywhere in tree)
|
|
236
|
+
const tree = [];
|
|
237
|
+
for (const parent of parentMap.values()) {
|
|
238
|
+
const totalMemories = parent.memories.length +
|
|
239
|
+
parent.children.reduce((sum, c) => sum + c.memories.length, 0);
|
|
240
|
+
if (totalMemories > 0) {
|
|
241
|
+
// Also filter out empty children
|
|
242
|
+
parent.children = parent.children.filter(c => c.memories.length > 0);
|
|
243
|
+
tree.push(parent);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return tree;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
// ═══════════════════════════════════════════
|
|
252
|
+
// INTERNAL: Pass 1 — Temporary placement
|
|
253
|
+
// ═══════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
function _layoutParents(tree) {
|
|
256
|
+
const n = tree.length;
|
|
257
|
+
if (n === 0) return;
|
|
258
|
+
if (n === 1) {
|
|
259
|
+
tree[0].cx = 0;
|
|
260
|
+
tree[0].cy = 0;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Temporary tight ring — will be replaced in pass 2
|
|
265
|
+
const baseRadius = gfx.parentOrbitRadius + n * 80;
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
const angle = (2 * Math.PI * i) / n - Math.PI / 2; // start at top
|
|
269
|
+
tree[i].cx = Math.cos(angle) * baseRadius;
|
|
270
|
+
tree[i].cy = Math.sin(angle) * baseRadius;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _layoutChildren(parent) {
|
|
275
|
+
const children = parent.children;
|
|
276
|
+
const n = children.length;
|
|
277
|
+
if (n === 0) return;
|
|
278
|
+
|
|
279
|
+
// Temporary placement — will be replaced after radii are known
|
|
280
|
+
const orbitRadius = gfx.childOrbitGap + n * 30;
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < n; i++) {
|
|
283
|
+
const angle = (2 * Math.PI * i) / n;
|
|
284
|
+
children[i].cx = parent.cx + Math.cos(angle) * orbitRadius;
|
|
285
|
+
children[i].cy = parent.cy + Math.sin(angle) * orbitRadius;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
// ═══════════════════════════════════════════
|
|
291
|
+
// INTERNAL: Label exclusion zone
|
|
292
|
+
// ═══════════════════════════════════════════
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Compute the minimum orbital radius so no card can overlap the category label.
|
|
296
|
+
* Uses Minkowski sum of label bounding box + card bounding box + margin,
|
|
297
|
+
* then takes the diagonal — guarantees no overlap at ANY angle on the ring.
|
|
298
|
+
*
|
|
299
|
+
* Must mirror the label sizes in graph.js _drawCategoryLabel().
|
|
300
|
+
*/
|
|
301
|
+
function _computeLabelExclusionRadius(region, isParent) {
|
|
302
|
+
const name = region.name;
|
|
303
|
+
const mul = state.labelSizeMultiplier || 1;
|
|
304
|
+
const MARGIN = 35; // breathing room between label edge and nearest card edge
|
|
305
|
+
const hasLogo = !!region.logoUrl;
|
|
306
|
+
|
|
307
|
+
let halfLabelW, halfLabelH;
|
|
308
|
+
|
|
309
|
+
if (isParent) {
|
|
310
|
+
const fontSize = 38 * mul;
|
|
311
|
+
if (hasLogo) {
|
|
312
|
+
// Logo only — no name pill (matches graph.js parent label with logo)
|
|
313
|
+
const logoH = 64 * mul;
|
|
314
|
+
const logoAspect = 3; // conservative estimate for wide logos
|
|
315
|
+
const logoW = logoH * logoAspect;
|
|
316
|
+
const countH = fontSize * 0.45 + 10;
|
|
317
|
+
const totalH = logoH + 6 + countH;
|
|
318
|
+
halfLabelW = logoW / 2;
|
|
319
|
+
halfLabelH = totalH / 2;
|
|
320
|
+
} else {
|
|
321
|
+
// Text only — name text + count
|
|
322
|
+
const textW = name.length * fontSize * 0.65;
|
|
323
|
+
const countH = fontSize * 0.45 + 10;
|
|
324
|
+
const totalH = fontSize + 4 + countH;
|
|
325
|
+
halfLabelW = textW / 2;
|
|
326
|
+
halfLabelH = totalH / 2;
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
const fontSize = 22 * mul;
|
|
330
|
+
if (hasLogo) {
|
|
331
|
+
// Logo only — no name text (matches graph.js child label with logo)
|
|
332
|
+
const logoH = 32 * mul;
|
|
333
|
+
const logoAspect = 3;
|
|
334
|
+
const logoW = logoH * logoAspect;
|
|
335
|
+
const countH = fontSize * 0.5 + 8;
|
|
336
|
+
const totalH = logoH + 4 + countH;
|
|
337
|
+
halfLabelW = logoW / 2;
|
|
338
|
+
halfLabelH = totalH / 2;
|
|
339
|
+
} else {
|
|
340
|
+
// Text only — name text + count
|
|
341
|
+
const textW = name.length * fontSize * 0.65;
|
|
342
|
+
const countH = fontSize * 0.5 + 8;
|
|
343
|
+
const totalH = fontSize + 4 + countH;
|
|
344
|
+
halfLabelW = textW / 2;
|
|
345
|
+
halfLabelH = totalH / 2;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Minkowski sum diagonal: guarantees no card at any ring angle can touch the label
|
|
350
|
+
const expandedW = halfLabelW + CARD_W / 2 + MARGIN;
|
|
351
|
+
const expandedH = halfLabelH + CARD_H / 2 + MARGIN;
|
|
352
|
+
return Math.ceil(Math.sqrt(expandedW * expandedW + expandedH * expandedH));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
// ═══════════════════════════════════════════
|
|
357
|
+
// INTERNAL: Card orbital layout
|
|
358
|
+
// ═══════════════════════════════════════════
|
|
359
|
+
|
|
360
|
+
function _layoutCards(region, memories, isParent) {
|
|
361
|
+
// Sort by importance descending (most important = innermost ring)
|
|
362
|
+
const sorted = [...memories].sort((a, b) => {
|
|
363
|
+
const impA = a.payload?.importance || 5;
|
|
364
|
+
const impB = b.payload?.importance || 5;
|
|
365
|
+
if (impB !== impA) return impB - impA;
|
|
366
|
+
// Secondary: alphabetical content for stability
|
|
367
|
+
const cA = (a.payload?.content || '').slice(0, 30);
|
|
368
|
+
const cB = (b.payload?.content || '').slice(0, 30);
|
|
369
|
+
return cA.localeCompare(cB);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const cards = [];
|
|
373
|
+
const gap = gfx.cardGap;
|
|
374
|
+
const ringGap = CARD_H + 20;
|
|
375
|
+
// Dynamic inner radius based on actual label dimensions — no card can touch the label
|
|
376
|
+
const innerRadius = _computeLabelExclusionRadius(region, isParent);
|
|
377
|
+
|
|
378
|
+
let placed = 0;
|
|
379
|
+
let ringIndex = 0;
|
|
380
|
+
|
|
381
|
+
while (placed < sorted.length) {
|
|
382
|
+
const ringRadius = innerRadius + ringIndex * ringGap;
|
|
383
|
+
const circumference = 2 * Math.PI * ringRadius;
|
|
384
|
+
const cardsInRing = Math.max(1, Math.floor(circumference / (CARD_W + gap)));
|
|
385
|
+
const actualCount = Math.min(cardsInRing, sorted.length - placed);
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < actualCount; i++) {
|
|
388
|
+
const angle = (2 * Math.PI * i) / actualCount;
|
|
389
|
+
const node = sorted[placed + i];
|
|
390
|
+
const card = {
|
|
391
|
+
node,
|
|
392
|
+
x: region.cx + Math.cos(angle) * ringRadius,
|
|
393
|
+
y: region.cy + Math.sin(angle) * ringRadius,
|
|
394
|
+
pinned: false,
|
|
395
|
+
};
|
|
396
|
+
cards.push(card);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
placed += actualCount;
|
|
400
|
+
ringIndex++;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Update region radius to encompass all rings
|
|
404
|
+
region.radius = innerRadius + Math.max(0, ringIndex - 1) * ringGap + CARD_H;
|
|
405
|
+
|
|
406
|
+
return cards;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// ═══════════════════════════════════════════
|
|
411
|
+
// INTERNAL: Pass 2 — Re-position with actual radii
|
|
412
|
+
// ═══════════════════════════════════════════
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Re-position children using their actual radii so they don't overlap each other.
|
|
416
|
+
*/
|
|
417
|
+
function _reLayoutChildren(parent) {
|
|
418
|
+
const children = parent.children;
|
|
419
|
+
const n = children.length;
|
|
420
|
+
if (n === 0) return;
|
|
421
|
+
|
|
422
|
+
// Determine orbit radius based on actual sizes
|
|
423
|
+
const maxChildRadius = Math.max(...children.map(c => c.radius || 60));
|
|
424
|
+
const parentRadius = parent.radius || 60;
|
|
425
|
+
|
|
426
|
+
// Orbit = parent region edge + child region edge + gap
|
|
427
|
+
const orbitRadius = parentRadius + maxChildRadius + 80;
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < n; i++) {
|
|
430
|
+
const angle = (2 * Math.PI * i) / n;
|
|
431
|
+
children[i].cx = parent.cx + Math.cos(angle) * orbitRadius;
|
|
432
|
+
children[i].cy = parent.cy + Math.sin(angle) * orbitRadius;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Re-position parents in a ring using their effective cluster radii
|
|
438
|
+
* so adjacent parents have proper spacing.
|
|
439
|
+
*/
|
|
440
|
+
function _reLayoutParentsWithRadii(tree) {
|
|
441
|
+
const n = tree.length;
|
|
442
|
+
if (n === 0) return;
|
|
443
|
+
if (n === 1) {
|
|
444
|
+
tree[0].cx = 0;
|
|
445
|
+
tree[0].cy = 0;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Calculate effective radius for each parent (includes child orbits)
|
|
450
|
+
const effectiveRadii = tree.map(parent => {
|
|
451
|
+
let maxExtent = parent.radius || 100;
|
|
452
|
+
for (const child of parent.children) {
|
|
453
|
+
const distFromParent = Math.sqrt(
|
|
454
|
+
(child.cx - parent.cx) ** 2 + (child.cy - parent.cy) ** 2
|
|
455
|
+
);
|
|
456
|
+
const childExtent = distFromParent + (child.radius || 60);
|
|
457
|
+
maxExtent = Math.max(maxExtent, childExtent);
|
|
458
|
+
}
|
|
459
|
+
return maxExtent;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Ring radius: ensure adjacent clusters don't overlap
|
|
463
|
+
// Sum of all effective diameters + gaps, divided by 2*PI
|
|
464
|
+
const minGap = 120;
|
|
465
|
+
const totalDiameter = effectiveRadii.reduce((sum, r) => sum + r * 2, 0);
|
|
466
|
+
const totalGap = n * minGap;
|
|
467
|
+
const ringRadius = Math.max(
|
|
468
|
+
(totalDiameter + totalGap) / (2 * Math.PI),
|
|
469
|
+
gfx.parentOrbitRadius
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
for (let i = 0; i < n; i++) {
|
|
473
|
+
const angle = (2 * Math.PI * i) / n - Math.PI / 2; // start at top
|
|
474
|
+
tree[i].cx = Math.cos(angle) * ringRadius;
|
|
475
|
+
tree[i].cy = Math.sin(angle) * ringRadius;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
// ═══════════════════════════════════════════
|
|
481
|
+
// INTERNAL: Delta propagation
|
|
482
|
+
// ═══════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* After regions are moved (re-layout or collision), propagate
|
|
486
|
+
* the position deltas to their cards.
|
|
487
|
+
*/
|
|
488
|
+
function _propagateDeltas(tree, allCards, oldPositions) {
|
|
489
|
+
// Build delta map: category name → { dx, dy }
|
|
490
|
+
const deltas = new Map();
|
|
491
|
+
for (const parent of tree) {
|
|
492
|
+
const old = oldPositions.get(parent.name);
|
|
493
|
+
if (old) deltas.set(parent.name, { dx: parent.cx - old.cx, dy: parent.cy - old.cy });
|
|
494
|
+
for (const child of parent.children) {
|
|
495
|
+
const oldC = oldPositions.get(child.name);
|
|
496
|
+
if (oldC) deltas.set(child.name, { dx: child.cx - oldC.cx, dy: child.cy - oldC.cy });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const card of allCards) {
|
|
501
|
+
const cat = card.node.payload?.category;
|
|
502
|
+
const delta = deltas.get(cat);
|
|
503
|
+
if (delta) {
|
|
504
|
+
card.x += delta.dx;
|
|
505
|
+
card.y += delta.dy;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
// ═══════════════════════════════════════════
|
|
512
|
+
// INTERNAL: Collision avoidance
|
|
513
|
+
// ═══════════════════════════════════════════
|
|
514
|
+
|
|
515
|
+
function _resolveCollisions(tree) {
|
|
516
|
+
const allRegions = flattenRegions(tree);
|
|
517
|
+
if (allRegions.length < 2) return;
|
|
518
|
+
|
|
519
|
+
// 6 passes with 80px padding, resolves cross-parent collisions
|
|
520
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
521
|
+
for (let i = 0; i < allRegions.length; i++) {
|
|
522
|
+
for (let j = i + 1; j < allRegions.length; j++) {
|
|
523
|
+
const a = allRegions[i];
|
|
524
|
+
const b = allRegions[j];
|
|
525
|
+
|
|
526
|
+
// Skip parent-child pairs (children orbit their parent deliberately)
|
|
527
|
+
if (a.isParent && !b.isParent && b.parentName === a.name) continue;
|
|
528
|
+
if (b.isParent && !a.isParent && a.parentName === b.name) continue;
|
|
529
|
+
|
|
530
|
+
const dx = b.cx - a.cx;
|
|
531
|
+
const dy = b.cy - a.cy;
|
|
532
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
533
|
+
const minDist = (a.radius || 100) + (b.radius || 100) + 80;
|
|
534
|
+
|
|
535
|
+
if (dist < minDist) {
|
|
536
|
+
const push = (minDist - dist) / 2;
|
|
537
|
+
const nx = dx / dist;
|
|
538
|
+
const ny = dy / dist;
|
|
539
|
+
|
|
540
|
+
a.cx -= nx * push;
|
|
541
|
+
a.cy -= ny * push;
|
|
542
|
+
b.cx += nx * push;
|
|
543
|
+
b.cy += ny * push;
|
|
544
|
+
|
|
545
|
+
// If a parent moved, cascade to its children
|
|
546
|
+
if (a.isParent) _moveChildrenBy(a, -nx * push, -ny * push);
|
|
547
|
+
if (b.isParent) _moveChildrenBy(b, nx * push, ny * push);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _moveChildrenBy(parent, dx, dy) {
|
|
555
|
+
if (!parent.children) return;
|
|
556
|
+
for (const child of parent.children) {
|
|
557
|
+
child.cx += dx;
|
|
558
|
+
child.cy += dy;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
// ═══════════════════════════════════════════
|
|
564
|
+
// INTERNAL: Position persistence
|
|
565
|
+
// ═══════════════════════════════════════════
|
|
566
|
+
|
|
567
|
+
function _restoreSavedPositions(tree, cards) {
|
|
568
|
+
let saved;
|
|
569
|
+
try {
|
|
570
|
+
saved = JSON.parse(storage.getItem(KEYS.NODE_POS_2D) || '{}');
|
|
571
|
+
} catch { return; }
|
|
572
|
+
if (!saved || typeof saved !== 'object') return;
|
|
573
|
+
|
|
574
|
+
// Restore category positions
|
|
575
|
+
const allRegions = flattenRegions(tree);
|
|
576
|
+
for (const region of allRegions) {
|
|
577
|
+
const key = `_cat_${region.name}`;
|
|
578
|
+
const pos = saved[key];
|
|
579
|
+
if (pos && pos.x != null && pos.y != null) {
|
|
580
|
+
region.cx = pos.x;
|
|
581
|
+
region.cy = pos.y;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Restore card positions
|
|
586
|
+
for (const card of cards) {
|
|
587
|
+
const pos = saved[card.node.id];
|
|
588
|
+
if (pos && pos.x != null && pos.y != null) {
|
|
589
|
+
card.x = pos.x;
|
|
590
|
+
card.y = pos.y;
|
|
591
|
+
card.pinned = pos.pinned || false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|