diff --git a/web/src/lib/refView.ts b/web/src/lib/refView.ts index 8204cc07..5a1c28f5 100644 --- a/web/src/lib/refView.ts +++ b/web/src/lib/refView.ts @@ -31,8 +31,8 @@ export interface ForceLink extends SimulationLinkDatum { // ── Degree radius ── -export function degreeRadius(degree: number, baseRadius: number = 8): number { - return Math.max(3, baseRadius - degree * 1.5) +export function degreeRadius(degree: number, baseRadius: number = 12): number { + return Math.max(4, baseRadius - degree * 1.5) } // ── Ref view types ── diff --git a/web/src/lib/server/skeleton.ts b/web/src/lib/server/skeleton.ts index b8500b2a..25e97117 100644 --- a/web/src/lib/server/skeleton.ts +++ b/web/src/lib/server/skeleton.ts @@ -69,7 +69,8 @@ function hslToHex(h: number, s: number, l: number): string { function sortToColor(sort: string): string { let h = 0 for (const c of sort) h = (Math.imul(h, 31) + c.charCodeAt(0)) >>> 0 - return hslToHex(h % 360, 50 + ((h >>> 8) % 30), 45 + ((h >>> 16) % 20)) + // 高饱和、中等亮度 → 鲜艳不透灰(与前端 sortColors 一致)。 + return hslToHex(h % 360, 72 + ((h >>> 8) % 23), 50 + ((h >>> 16) % 12)) } function blendHex(a: string, b: string): string { @@ -120,13 +121,13 @@ export function buildSkeletonView( let radii: NumMap = {} if (['degree', 'in-degree', 'out-degree'].includes(size)) { const mode = size === 'degree' ? 'total' : size === 'in-degree' ? 'in' : 'out' - radii = normalize(perSource(ents, (g) => degreeMetric(g, mode as 'total' | 'in' | 'out')), 3, 14) + radii = normalize(perSource(ents, (g) => degreeMetric(g, mode as 'total' | 'in' | 'out')), 4, 16) } else if (['pagerank', 'betweenness', 'katz', 'hub', 'authority'].includes(size)) { - radii = normalize(perSource(ents, (g) => centralityMetric(g, size)), 3, 14) + radii = normalize(perSource(ents, (g) => centralityMetric(g, size)), 4, 16) } else if (['depth', 'reachability'].includes(size)) { - radii = normalize(perSource(ents, (g) => dagMetric(g, size as 'depth' | 'reachability')), 3, 14) + radii = normalize(perSource(ents, (g) => dagMetric(g, size as 'depth' | 'reachability')), 4, 16) } else { - for (const id of nodes.keys()) radii[id] = 6.0 // uniform + for (const id of nodes.keys()) radii[id] = 9.0 // uniform } // ── colour ── diff --git a/web/src/lib/sortColors.ts b/web/src/lib/sortColors.ts index e49cab6b..d24c4352 100644 --- a/web/src/lib/sortColors.ts +++ b/web/src/lib/sortColors.ts @@ -23,7 +23,8 @@ function hsl2hex(h: number, s: number, l: number): string { function atomicColor(sort: string): string { if (_cache[sort]) return _cache[sort] const h = hash(sort) - const color = hsl2hex(h % 360, (50 + (h >> 8) % 30) / 100, (45 + (h >> 16) % 20) / 100) + // 高饱和、中等亮度 → 鲜艳不透灰。 + const color = hsl2hex(h % 360, (72 + (h >> 8) % 23) / 100, (50 + (h >> 16) % 12) / 100) _cache[sort] = color return color } diff --git a/web/src/panels/workspace/NetworkView.tsx b/web/src/panels/workspace/NetworkView.tsx index bf5b2c3f..4e5d9b61 100644 --- a/web/src/panels/workspace/NetworkView.tsx +++ b/web/src/panels/workspace/NetworkView.tsx @@ -27,9 +27,9 @@ import { NetworkSettings } from './NetworkSettings' import { usePluginStore } from '@/plugins/registry' // 常规节点半径(refView 的 degreeRadius 基准值)——连线粗细以此为锚等比缩放。 -const BASE_NODE_RADIUS = 8 +const BASE_NODE_RADIUS = 12 // 连线粗细 = 常规节点半径的固定比例(世界单位),随缩放与节点同步放大。 -const LINK_WIDTH_RATIO = 0.1 +const LINK_WIDTH_RATIO = 0.04 // 默认连线颜色:亮灰色。 const LINK_COLOR = 'rgba(200,200,210,0.5)' @@ -49,6 +49,67 @@ function drawArrow(ctx: CanvasRenderingContext2D, sx: number, sy: number, tx: nu ctx.fill() } +// LeanNets 图设置由设置面板写入 usePluginStore(registry)。这里统一带默认值读取, +// 取代之前从 window.__pluginStore 全局镜像读取的写法。 +interface MnSettings { + sizeBy: string; colorBy: string; clusterBy: string + sourceFilter: string; mergeProofs: boolean +} +function readMnSettings(): MnSettings { + const s = usePluginStore.getState() as any + return { + sizeBy: s.mnSizeBy || 'uniform', + colorBy: s.mnColorBy || 'sort', + clusterBy: s.mnCluster || 'none', + sourceFilter: s.mnSource || 'all', + mergeProofs: s.mnMergeProofs || false, + } +} + +// cluster 力:把同簇节点拉向各自质心。强度每帧实时读,滑块即时生效。 +function makeClusterForce(getNodes: () => ForceNode[]) { + return (alpha: number) => { + const nodes = getNodes() + const centroids: Record = {} + for (const n of nodes) { + if (n.cluster === undefined || n.x == null || n.y == null) continue + if (!centroids[n.cluster]) centroids[n.cluster] = { x: 0, y: 0, count: 0 } + centroids[n.cluster].x += n.x + centroids[n.cluster].y += n.y + centroids[n.cluster].count++ + } + for (const c of Object.values(centroids)) { c.x /= c.count; c.y /= c.count } + const clusterStrength = ((usePluginStore.getState() as any).mnClusterStrength ?? 30) / 100 + const strength = clusterStrength * alpha + for (const n of nodes) { + if (n.cluster === undefined || n.x == null || n.y == null) continue + const c = centroids[n.cluster] + if (!c) continue + n.vx = (n.vx || 0) + (c.x - n.x) * strength + n.vy = (n.vy || 0) + (c.y - n.y) * strength + } + } +} + +// 把全量原子的颜色算出来写到 window.__skeletonColors(供文档卡片着色用)。 +// overlay 覆盖已就地更新的节点色;fallbackNodes 在 all-source 请求失败时兜底。 +function propagateAtomColors( + endpoint: string, path: string, colorBy: string, + fallbackNodes: any[], overlay: Record, isAlive: () => boolean, +) { + fetch(`${API_BASE}${endpoint}?path=${encodeURIComponent(path)}&source=all&size=uniform&color=${colorBy}`) + .then(r => r.ok ? r.json() : null) + .then(allData => { + if (!isAlive()) return + const map: Record = {} + for (const n of (allData?.nodes || fallbackNodes || [])) map[n.id] = n.color + for (const [k, v] of Object.entries(overlay)) map[k] = v + ;(window as any).__skeletonColors = map + import('@/lib/entryColor').then(m => m.notifyColorsUpdated()) + }) + .catch(() => {}) +} + export const NetworkView = memo(function NetworkView() { // ── Store 订阅 ── const selectedObjHash = useSelectObjStore(s => s.selectedHash) @@ -185,9 +246,7 @@ export const NetworkView = memo(function NetworkView() { ctx.beginPath() ctx.arc(node.x, node.y, r, 0, 2 * Math.PI) ctx.fillStyle = isSelected ? '#ffffff' : node.color - ctx.globalAlpha = currentSelectedObj && !isSelected && !isHovered ? 0.6 : 1 ctx.fill() - ctx.globalAlpha = 1 // State ring const stateColor = node.state === 'proven' ? '#22c55e' : node.state === 'sorry' ? '#eab308' : node.state === 'error' ? '#ef4444' : null @@ -424,12 +483,7 @@ export const NetworkView = memo(function NetworkView() { const path = new URLSearchParams(window.location.search).get('path') if (!path) return - // Read settings from plugin store (if available) - let sizeBy = 'uniform', colorBy = 'sort', clusterBy = 'none', sourceFilter = 'all', mergeProofs = false - try { - const store = (window as any).__pluginStore - if (store) { sizeBy = store.mnSizeBy || 'uniform'; colorBy = store.mnColorBy || 'sort'; clusterBy = store.mnCluster || 'none'; sourceFilter = store.mnSource || 'all'; mergeProofs = store.mnMergeProofs || false } - } catch {} + const { sizeBy, colorBy, clusterBy, sourceFilter, mergeProofs } = readMnSettings() const activeMode = usePluginStore.getState().getActiveNetworkMode() // 在 effect 内读取最新模式,避免用到闭包里 stale 的 networkMode。 @@ -463,17 +517,7 @@ export const NetworkView = memo(function NetworkView() { ...(e.dashed ? { dashed: true } : {}), })) // Propagate colors to ALL atoms (not just filtered) - const p = encodeURIComponent(path) - fetch(`${API_BASE}${activeMode!.endpoint}?path=${p}&source=all&size=uniform&color=${colorBy}`) - .then(r => r.ok ? r.json() : null) - .then(allData => { - if (!alive) return - const colorMap: Record = {} - for (const n of (allData?.nodes || data.nodes || [])) colorMap[n.id] = n.color - ;(window as any).__skeletonColors = colorMap - import('@/lib/entryColor').then(m => m.notifyColorsUpdated()) - }) - .catch(() => {}) + propagateAtomColors(activeMode!.endpoint, path, colorBy, data.nodes, {}, () => alive) } else { ;(window as any).__skeletonColors = null import('@/lib/entryColor').then(m => m.notifyColorsUpdated()) @@ -497,34 +541,7 @@ export const NetworkView = memo(function NetworkView() { // Cluster force: pull nodes toward cluster centroid const hasCluster = forceNodes.some(n => n.cluster !== undefined) - if (hasCluster) { - sim.force('cluster', (alpha: number) => { - // Compute cluster centroids - const centroids: Record = {} - for (const n of forceNodes) { - if (n.cluster === undefined || n.x == null || n.y == null) continue - if (!centroids[n.cluster]) centroids[n.cluster] = { x: 0, y: 0, count: 0 } - centroids[n.cluster].x += n.x - centroids[n.cluster].y += n.y - centroids[n.cluster].count++ - } - for (const c of Object.values(centroids)) { - c.x /= c.count; c.y /= c.count - } - // Pull toward centroid - const clusterStrength = ((window as any).__pluginStore?.mnClusterStrength ?? 30) / 100 - const strength = clusterStrength * alpha - for (const n of forceNodes) { - if (n.cluster === undefined || n.x == null || n.y == null) continue - const c = centroids[n.cluster] - if (!c) continue - n.vx = (n.vx || 0) + (c.x - n.x) * strength - n.vy = (n.vy || 0) + (c.y - n.y) * strength - } - }) - } else { - sim.force('cluster', null) - } + sim.force('cluster', hasCluster ? makeClusterForce(() => nodesRef.current) : null) sim.alpha(1).restart() }) @@ -538,11 +555,7 @@ export const NetworkView = memo(function NetworkView() { const path = new URLSearchParams(window.location.search).get('path') if (!path) return - let sizeBy = 'uniform', colorBy = 'sort', clusterBy = 'none', sourceFilter = 'all', mergeProofs = false - try { - const store = (window as any).__pluginStore - if (store) { sizeBy = store.mnSizeBy || 'uniform'; colorBy = store.mnColorBy || 'sort'; clusterBy = store.mnCluster || 'none'; sourceFilter = store.mnSource || 'all'; mergeProofs = store.mnMergeProofs || false } - } catch {} + const { sizeBy, colorBy, clusterBy, sourceFilter, mergeProofs } = readMnSettings() const modeForStyle = usePluginStore.getState().getActiveNetworkMode() if (!modeForStyle) return @@ -569,46 +582,14 @@ export const NetworkView = memo(function NetworkView() { } } // Propagate colors to ALL atoms - const p2 = encodeURIComponent(new URLSearchParams(window.location.search).get('path') || '') - fetch(`${API_BASE}${modeForStyle.endpoint}?path=${p2}&source=all&size=uniform&color=${colorBy}`) - .then(r => r.ok ? r.json() : null) - .then(allData => { - if (!alive) return - const fullMap: Record = {} - for (const n of (allData?.nodes || [])) fullMap[n.id] = n.color - // Merge with current nodes (they may have updated colors) - for (const [k, v] of Object.entries(colorMap)) fullMap[k] = v - ;(window as any).__skeletonColors = fullMap - import('@/lib/entryColor').then(m => m.notifyColorsUpdated()) - }) - .catch(() => {}) + propagateAtomColors(modeForStyle.endpoint, path, colorBy, [], colorMap, () => alive) // Update cluster force const sim = simulationRef.current if (sim) { const hasCluster = nodesRef.current.some(n => n.cluster !== undefined) - if (hasCluster) { - sim.force('cluster', (alpha: number) => { - const centroids: Record = {} - for (const n of nodesRef.current) { - if (n.cluster === undefined || n.x == null || n.y == null) continue - if (!centroids[n.cluster]) centroids[n.cluster] = { x: 0, y: 0, count: 0 } - centroids[n.cluster].x += n.x; centroids[n.cluster].y += n.y; centroids[n.cluster].count++ - } - for (const c of Object.values(centroids)) { c.x /= c.count; c.y /= c.count } - const clusterStrength = ((window as any).__pluginStore?.mnClusterStrength ?? 30) / 100 - const strength = clusterStrength * alpha - for (const n of nodesRef.current) { - if (n.cluster === undefined || n.x == null || n.y == null) continue - const c = centroids[n.cluster]; if (!c) continue - n.vx = (n.vx || 0) + (c.x - n.x) * strength - n.vy = (n.vy || 0) + (c.y - n.y) * strength - } - }) - sim.alpha(0.3).restart() - } else { - sim.force('cluster', null) - } + sim.force('cluster', hasCluster ? makeClusterForce(() => nodesRef.current) : null) + if (hasCluster) sim.alpha(0.3).restart() } // Update edge colors diff --git a/web/src/plugins/leannets/SettingsPanel.tsx b/web/src/plugins/leannets/SettingsPanel.tsx index f6b33eb3..6f973e05 100644 --- a/web/src/plugins/leannets/SettingsPanel.tsx +++ b/web/src/plugins/leannets/SettingsPanel.tsx @@ -18,8 +18,6 @@ export function LeanNetsSettings() { const set = (key: string, value: any) => { usePluginStore.setState({ [key]: value } as any) - if (!(window as any).__pluginStore) (window as any).__pluginStore = {} - ;(window as any).__pluginStore[key] = value if (key === 'mnSource' || key === 'mnMergeProofs') { window.dispatchEvent(new CustomEvent('mn-source-changed')) } else {