@andespindola/brainlink 0.1.0-beta.100 → 0.1.0-beta.102

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/README.md CHANGED
@@ -595,6 +595,7 @@ The graph UI shows:
595
595
  - realtime refresh while `--watch` is enabled
596
596
  - graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
597
597
  - wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
598
+ - zoom-out floor for large and massive graphs, plus reset macro floor tied to hub-neighbor distance and first-level cluster spacing, so initial macro view stays closer to first particle layers instead of over-distancing
598
599
  - keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
599
600
  - double-click on canvas zooms in at cursor position
600
601
  - floating graph totals (notes, links, tags) below the Brainlink title
@@ -2213,7 +2213,19 @@ const currentZoomMax = () => {
2213
2213
  return Math.max(zoomRange.min * 2, capped)
2214
2214
  }
2215
2215
 
2216
- const clampScale = value => Math.max(zoomRange.min, Math.min(currentZoomMax(), value))
2216
+ const zoomFloorByNodeCount = (nodeCount) => {
2217
+ if (nodeCount > massiveGraphNodeThreshold) return 0.0028
2218
+ if (nodeCount > largeGraphNodeThreshold) return 0.0014
2219
+ if (nodeCount > ecosystemActivationNodeThreshold) return 0.0006
2220
+ return zoomRange.min
2221
+ }
2222
+
2223
+ const currentZoomMin = () => {
2224
+ const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
2225
+ return Math.max(zoomRange.min, zoomFloorByNodeCount(nodeCount))
2226
+ }
2227
+
2228
+ const clampScale = value => Math.max(currentZoomMin(), Math.min(currentZoomMax(), value))
2217
2229
  const isFiniteNumber = value => Number.isFinite(value)
2218
2230
  const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
2219
2231
  const clampTransformCoordinate = value => {
@@ -2270,6 +2282,56 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
2270
2282
  return { min: 0.0012, max: 0.24 }
2271
2283
  }
2272
2284
 
2285
+ const macroFaceToFaceScale = (nodeCount, hubDistance) => {
2286
+ if (!Number.isFinite(hubDistance) || hubDistance <= 0 || nodeCount <= ecosystemActivationNodeThreshold) {
2287
+ return 0
2288
+ }
2289
+
2290
+ const rect = canvas.getBoundingClientRect()
2291
+ const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
2292
+ const share = nodeCount > massiveGraphNodeThreshold ? 0.052 : 0.046
2293
+ const targetPx = Math.max(18, viewportReference * share)
2294
+ return targetPx / hubDistance
2295
+ }
2296
+
2297
+ const nearestClusterNeighborDistance = (clusters) => {
2298
+ if (!Array.isArray(clusters) || clusters.length < 2) {
2299
+ return Number.POSITIVE_INFINITY
2300
+ }
2301
+
2302
+ let nearestDistance = Number.POSITIVE_INFINITY
2303
+ for (let index = 0; index < clusters.length; index += 1) {
2304
+ const source = clusters[index]
2305
+ for (let neighborIndex = index + 1; neighborIndex < clusters.length; neighborIndex += 1) {
2306
+ const target = clusters[neighborIndex]
2307
+ const distance = Math.hypot(source.x - target.x, source.y - target.y)
2308
+ if (distance > 0 && distance < nearestDistance) {
2309
+ nearestDistance = distance
2310
+ }
2311
+ }
2312
+ }
2313
+
2314
+ return nearestDistance
2315
+ }
2316
+
2317
+ const macroEcosystemFaceScale = (nodeCount) => {
2318
+ if (nodeCount <= ecosystemActivationNodeThreshold) {
2319
+ return 0
2320
+ }
2321
+
2322
+ const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
2323
+ const siblingClusters = baseClusters.filter(cluster => !cluster.isHub)
2324
+ const nearestDistance = nearestClusterNeighborDistance(siblingClusters)
2325
+ if (!Number.isFinite(nearestDistance) || nearestDistance <= 0) {
2326
+ return 0
2327
+ }
2328
+
2329
+ const rect = canvas.getBoundingClientRect()
2330
+ const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
2331
+ const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.068 : 0.058
2332
+ const targetPx = Math.max(22, viewportReference * targetShare)
2333
+ return targetPx / nearestDistance
2334
+ }
2273
2335
  const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
2274
2336
  const rect = canvas.getBoundingClientRect()
2275
2337
  const width = Math.max(rect.width, 320)
@@ -2307,6 +2369,15 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
2307
2369
  : nodes.length > massiveGraphNodeThreshold
2308
2370
  ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
2309
2371
  : baselineScale
2372
+ const macroFloorScale = options.macro
2373
+ ? clampScale(Math.max(
2374
+ macroFaceToFaceScale(nodes.length, state.hubNeighborDistance),
2375
+ macroEcosystemFaceScale(nodes.length)
2376
+ ))
2377
+ : 0
2378
+ const resolvedScale = options.macro
2379
+ ? clampScale(Math.max(scale, macroFloorScale))
2380
+ : scale
2310
2381
  const hubCenter =
2311
2382
  options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
2312
2383
  ? state.primaryHub
@@ -2315,9 +2386,9 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
2315
2386
  const centerY = hubCenter ? hubCenter.y : (bounds.minY + bounds.maxY) / 2
2316
2387
 
2317
2388
  state.transform = {
2318
- x: clampTransformCoordinate(width / 2 - centerX * scale),
2319
- y: clampTransformCoordinate(height / 2 - centerY * scale),
2320
- scale: clampScale(scale)
2389
+ x: clampTransformCoordinate(width / 2 - centerX * resolvedScale),
2390
+ y: clampTransformCoordinate(height / 2 - centerY * resolvedScale),
2391
+ scale: clampScale(resolvedScale)
2321
2392
  }
2322
2393
  state.offscreenFrameCount = 0
2323
2394
  state.recoveringViewport = false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.100",
3
+ "version": "0.1.0-beta.102",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",