@andespindola/brainlink 0.1.0-beta.87 → 0.1.0-beta.89

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
@@ -84,7 +84,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
84
84
  - Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
85
85
  - Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
86
86
  - WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
87
- - Graph zoom-out renders hierarchical ecosystem subgraphs only above 1000 notes: distant groups stay as small sand-like points and expand near the user's focus into smaller graph meshes before individual notes are rendered.
87
+ - Graph zoom-out renders hierarchical ecosystem subgraphs only above 1000 notes: the memory hub stays centered, 1000-note groups stay as compact sand-like points, and focused groups gradually expand into smaller graph meshes before individual notes are rendered.
88
88
  - Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
89
89
  - Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
90
90
  - Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
@@ -602,7 +602,7 @@ The graph UI shows:
602
602
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
603
603
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
604
604
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
605
- - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use connected sand-like ecosystem points of up to 1000 notes, zoom-in spreads only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
605
+ - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use a compact memory-hub-centered mesh of connected 1000-note points, zoom-in spreads only focused clusters into 250-note and 60-note subgraphs with aggregated real links, keeps parent points during expansion to avoid visual jumps, then progressively raises the focused node budget so local areas keep nearby notes and links visible
606
606
 
607
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
608
608
 
@@ -25,6 +25,7 @@ const ecosystemGroupSize = 1000
25
25
  const ecosystemActivationNodeThreshold = 1000
26
26
  const ecosystemGroupSizes = [1000, 250, 60]
27
27
  const ecosystemClusterEdgeLimit = 520
28
+ const ecosystemHubEdgeLimit = 120
28
29
  const ecosystemClusterScaleThreshold = 0.32
29
30
  const ecosystemSubgraphScaleThreshold = 0.18
30
31
  const ecosystemMicroScaleThreshold = 0.08
@@ -37,9 +38,9 @@ const meshEdgeMaxBudget = 1400
37
38
  const layeredCoreScaleThreshold = 0.55
38
39
  const dragNeighborhoodMaxAffected = 180
39
40
  const dragSettleRounds = 3
40
- const wheelZoomExponent = 0.0018
41
- const wheelZoomExponentCap = 0.09
42
- const wheelZoomModifierBoost = 1.22
41
+ const wheelZoomExponent = 0.0009
42
+ const wheelZoomExponentCap = 0.035
43
+ const wheelZoomModifierBoost = 1.08
43
44
  const state = {
44
45
  graph: { nodes: [], edges: [] },
45
46
  nodes: [],
@@ -74,6 +75,8 @@ const state = {
74
75
  visibleEdgeByNode: new Map(),
75
76
  ecosystemClusters: [],
76
77
  ecosystemClustersBySize: new Map(),
78
+ ecosystemNodeClusterBySize: new Map(),
79
+ ecosystemHubCluster: null,
77
80
  macroCenter: { x: 0, y: 0 },
78
81
  macroRepresentative: null,
79
82
  primaryHub: null,
@@ -570,11 +573,6 @@ const recomputeVisibility = () => {
570
573
  state.visibleEdges = limitedEdges
571
574
  state.visibleNodeSpatial = createSpatialIndex(nodes)
572
575
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
573
- const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
574
- ? buildEcosystemGraph(nodes)
575
- : { clusters: [], clustersBySize: new Map() }
576
- state.ecosystemClusters = ecosystemGraph.clusters
577
- state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
578
576
  const primaryHub = rankedHubNodes()[0] ?? null
579
577
  state.primaryHub = primaryHub
580
578
  state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
@@ -586,6 +584,13 @@ const recomputeVisibility = () => {
586
584
  y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
587
585
  }
588
586
  : { x: 0, y: 0 }
587
+ const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
588
+ ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
589
+ : { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
590
+ state.ecosystemClusters = ecosystemGraph.clusters
591
+ state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
592
+ state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
593
+ state.ecosystemHubCluster = ecosystemGraph.hubCluster
589
594
  state.macroRepresentative = resolveMacroRepresentative(nodes)
590
595
  markRenderDirty()
591
596
  }
@@ -730,18 +735,32 @@ const selectEcosystemRepresentative = nodes => {
730
735
  return representative
731
736
  }
732
737
 
733
- const buildEcosystemCluster = (nodes, index) => {
738
+ const ecosystemLayoutSpacingForSize = size => {
739
+ if (size >= 1000) return 360
740
+ if (size >= 250) return 92
741
+ return 28
742
+ }
743
+
744
+ const ecosystemCompactPoint = (index, total, center, spacing) => {
745
+ if (total <= 1) {
746
+ return { x: center.x, y: center.y }
747
+ }
748
+ const angle = index * 2.399963229728653
749
+ const radius = spacing * Math.sqrt(index + 1)
750
+ return {
751
+ x: center.x + Math.cos(angle) * radius,
752
+ y: center.y + Math.sin(angle) * radius
753
+ }
754
+ }
755
+
756
+ const buildEcosystemCluster = (nodes, index, point) => {
734
757
  const count = Math.max(nodes.length, 1)
735
- const sum = nodes.reduce((accumulator, node) => ({
736
- x: accumulator.x + node.x,
737
- y: accumulator.y + node.y
738
- }), { x: 0, y: 0 })
739
758
  const representative = selectEcosystemRepresentative(nodes)
740
759
 
741
760
  return {
742
761
  id: 'ecosystem-' + index,
743
- x: sum.x / count,
744
- y: sum.y / count,
762
+ x: point.x,
763
+ y: point.y,
745
764
  count,
746
765
  nodeIds: nodes.map(node => node.id),
747
766
  representative,
@@ -749,15 +768,42 @@ const buildEcosystemCluster = (nodes, index) => {
749
768
  }
750
769
  }
751
770
 
752
- const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
771
+ const buildEcosystemHubCluster = (hub, center) => hub
772
+ ? {
773
+ id: 'ecosystem-hub',
774
+ x: center.x,
775
+ y: center.y,
776
+ count: 1,
777
+ size: 1,
778
+ nodeIds: [hub.id],
779
+ representative: hub,
780
+ label: hub.title || 'Memory Hub',
781
+ parentId: null,
782
+ parentX: null,
783
+ parentY: null,
784
+ isHub: true
785
+ }
786
+ : null
787
+
788
+ const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
753
789
  const clusters = []
754
790
  const clusterByNodeId = new Map()
791
+ const parentChildIndex = new Map()
755
792
 
756
793
  for (let offset = 0; offset < sortedNodes.length; offset += size) {
757
794
  const clusterNodes = sortedNodes.slice(offset, offset + size)
758
795
  const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
796
+ const siblingIndex = parentCluster
797
+ ? (parentChildIndex.get(parentCluster.id) ?? 0)
798
+ : clusters.length
799
+ if (parentCluster) {
800
+ parentChildIndex.set(parentCluster.id, siblingIndex + 1)
801
+ }
802
+ const point = parentCluster
803
+ ? ecosystemCompactPoint(siblingIndex, Math.ceil((parentCluster.count || size) / size), parentCluster, ecosystemLayoutSpacingForSize(size))
804
+ : ecosystemCompactPoint(clusters.length, Math.ceil(sortedNodes.length / size), center, ecosystemLayoutSpacingForSize(size))
759
805
  const cluster = {
760
- ...buildEcosystemCluster(clusterNodes, clusters.length),
806
+ ...buildEcosystemCluster(clusterNodes, clusters.length, point),
761
807
  id: 'ecosystem-' + size + '-' + clusters.length,
762
808
  size,
763
809
  parentId: parentCluster?.id ?? null,
@@ -773,25 +819,32 @@ const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
773
819
  return { clusters, clusterByNodeId }
774
820
  }
775
821
 
776
- const buildEcosystemGraph = (nodes) => {
822
+ const buildEcosystemGraph = (nodes, center, hub) => {
777
823
  if (nodes.length === 0) {
778
- return { clusters: [], clustersBySize: new Map() }
824
+ return { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
779
825
  }
780
826
 
781
- const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
827
+ const hubCluster = buildEcosystemHubCluster(hub, center)
828
+ const sortedNodes = nodes
829
+ .filter(node => node.id !== hub?.id)
830
+ .sort(compareNodesForEcosystem)
782
831
  const clustersBySize = new Map()
832
+ const nodeClusterBySize = new Map()
783
833
  let parentLookup = null
784
834
 
785
835
  for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
786
836
  const size = ecosystemGroupSizes[index]
787
- const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
837
+ const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
788
838
  clustersBySize.set(size, level.clusters)
839
+ nodeClusterBySize.set(size, level.clusterByNodeId)
789
840
  parentLookup = level.clusterByNodeId
790
841
  }
791
842
 
792
843
  return {
793
844
  clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
794
- clustersBySize
845
+ clustersBySize,
846
+ nodeClusterBySize,
847
+ hubCluster
795
848
  }
796
849
  }
797
850
 
@@ -832,21 +885,24 @@ const smoothStep = value => {
832
885
  const zoomProgress = (scale, start, end) =>
833
886
  smoothStep((scale - start) / Math.max(end - start, 0.0001))
834
887
 
835
- const ecosystemPlanForScale = scale => {
836
- if (scale <= ecosystemMicroScaleThreshold) {
837
- return { baseSize: 1000, childSize: null, spread: 0 }
838
- }
839
- if (scale <= ecosystemSubgraphScaleThreshold) {
840
- return {
841
- baseSize: 1000,
842
- childSize: 250,
843
- spread: zoomProgress(scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
844
- }
845
- }
888
+ const shouldReplaceParentCluster = spread => spread >= 0.96
889
+
890
+ const expandFocusedClusters = (parentClusters, childSize, spread, viewport) => {
891
+ const focusPoint = ecosystemFocusPoint()
892
+ const expandedParentIds = new Set(nearestEcosystemParentIds(
893
+ parentClusters,
894
+ focusPoint,
895
+ ecosystemFocusedParentLimit
896
+ ))
897
+ const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
898
+ const visibleChildClusters = childClusters
899
+ .filter(cluster => expandedParentIds.has(cluster.parentId))
900
+ .map(cluster => spreadChildClusterFromParent(cluster, spread))
901
+ .filter(cluster => isClusterInViewport(cluster, viewport))
902
+
846
903
  return {
847
- baseSize: 250,
848
- childSize: 60,
849
- spread: zoomProgress(scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold)
904
+ expandedParentIds,
905
+ childClusters: visibleChildClusters
850
906
  }
851
907
  }
852
908
 
@@ -863,50 +919,93 @@ const spreadChildClusterFromParent = (cluster, spread) => {
863
919
  }
864
920
 
865
921
  const selectHierarchicalEcosystemClusters = viewport => {
866
- const plan = ecosystemPlanForScale(state.transform.scale)
867
- const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
922
+ const baseClusters = state.ecosystemClustersBySize.get(ecosystemGroupSize) ?? state.ecosystemClusters
868
923
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
924
+ const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
869
925
 
870
- if (!plan.childSize) {
871
- return visibleBaseClusters
926
+ if (state.transform.scale <= ecosystemMicroScaleThreshold) {
927
+ return [...hubClusters, ...visibleBaseClusters]
872
928
  }
873
929
 
874
- const focusPoint = ecosystemFocusPoint()
875
- const expandedParentIds = new Set(nearestEcosystemParentIds(
930
+ const midExpansion = expandFocusedClusters(
876
931
  visibleBaseClusters,
877
- focusPoint,
878
- ecosystemFocusedParentLimit
879
- ))
880
- const childClusters = state.ecosystemClustersBySize.get(plan.childSize) ?? []
881
- const visibleChildClusters = childClusters.filter(cluster =>
882
- expandedParentIds.has(cluster.parentId) &&
883
- isClusterInViewport(cluster, viewport)
884
- ).map(cluster => spreadChildClusterFromParent(cluster, plan.spread))
932
+ 250,
933
+ state.transform.scale <= ecosystemSubgraphScaleThreshold
934
+ ? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
935
+ : 1,
936
+ viewport
937
+ )
938
+ if (midExpansion.childClusters.length === 0) {
939
+ return [...hubClusters, ...visibleBaseClusters]
940
+ }
885
941
 
886
- if (visibleChildClusters.length === 0) {
887
- return visibleBaseClusters
942
+ const midSpread = state.transform.scale <= ecosystemSubgraphScaleThreshold
943
+ ? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
944
+ : 1
945
+ const remainingBaseClusters = shouldReplaceParentCluster(midSpread)
946
+ ? visibleBaseClusters.filter(cluster => !midExpansion.expandedParentIds.has(cluster.id))
947
+ : visibleBaseClusters
948
+ if (state.transform.scale <= ecosystemSubgraphScaleThreshold) {
949
+ return [
950
+ ...hubClusters,
951
+ ...remainingBaseClusters,
952
+ ...midExpansion.childClusters
953
+ ]
888
954
  }
889
955
 
956
+ const microExpansion = expandFocusedClusters(
957
+ midExpansion.childClusters,
958
+ 60,
959
+ zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold),
960
+ viewport
961
+ )
962
+ if (microExpansion.childClusters.length === 0) {
963
+ return [
964
+ ...hubClusters,
965
+ ...remainingBaseClusters,
966
+ ...midExpansion.childClusters
967
+ ]
968
+ }
969
+
970
+ const microSpread = zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold)
971
+ const visibleMidClusters = shouldReplaceParentCluster(microSpread)
972
+ ? midExpansion.childClusters.filter(cluster => !microExpansion.expandedParentIds.has(cluster.id))
973
+ : midExpansion.childClusters
890
974
  return [
891
- ...visibleBaseClusters.filter(cluster => !expandedParentIds.has(cluster.id)),
892
- ...visibleChildClusters
975
+ ...hubClusters,
976
+ ...remainingBaseClusters,
977
+ ...visibleMidClusters,
978
+ ...microExpansion.childClusters
893
979
  ]
894
980
  }
895
981
 
896
982
  const ecosystemEdgesForClusters = clusters => {
897
- const clusterByNodeId = new Map()
898
- for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
899
- const cluster = clusters[clusterIndex]
900
- for (let nodeIndex = 0; nodeIndex < cluster.nodeIds.length; nodeIndex += 1) {
901
- clusterByNodeId.set(cluster.nodeIds[nodeIndex], cluster)
983
+ const clusterById = new Map(clusters.map(cluster => [cluster.id, cluster]))
984
+ const clusterIds = new Set(clusterById.keys())
985
+ const levelBySize = new Map()
986
+ for (let index = 0; index < clusters.length; index += 1) {
987
+ const cluster = clusters[index]
988
+ if (!cluster.size || cluster.isHub) continue
989
+ levelBySize.set(cluster.size, state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map())
990
+ }
991
+ const resolveClusterForNode = nodeId => {
992
+ if (state.ecosystemHubCluster?.nodeIds.includes(nodeId) && clusterIds.has(state.ecosystemHubCluster.id)) {
993
+ return state.ecosystemHubCluster
994
+ }
995
+ for (const [size, lookup] of levelBySize) {
996
+ const cluster = lookup.get(nodeId)
997
+ if (cluster && clusterIds.has(cluster.id)) {
998
+ return clusterById.get(cluster.id) ?? cluster
999
+ }
902
1000
  }
1001
+ return null
903
1002
  }
904
1003
 
905
1004
  const edgeByClusterPair = new Map()
906
1005
  for (let index = 0; index < state.visibleEdges.length; index += 1) {
907
1006
  const edge = state.visibleEdges[index]
908
- const sourceCluster = clusterByNodeId.get(edge.source)
909
- const targetCluster = clusterByNodeId.get(edge.target)
1007
+ const sourceCluster = resolveClusterForNode(edge.source)
1008
+ const targetCluster = resolveClusterForNode(edge.target)
910
1009
  if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
911
1010
  continue
912
1011
  }
@@ -929,9 +1028,34 @@ const ecosystemEdgesForClusters = clusters => {
929
1028
  })
930
1029
  }
931
1030
 
932
- return Array.from(edgeByClusterPair.values())
1031
+ const edges = Array.from(edgeByClusterPair.values())
933
1032
  .sort((left, right) => right.weight - left.weight)
934
1033
  .slice(0, ecosystemClusterEdgeLimit)
1034
+ const hubCluster = state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)
1035
+ ? state.ecosystemHubCluster
1036
+ : null
1037
+ if (!hubCluster) {
1038
+ return edges
1039
+ }
1040
+
1041
+ const existingHubTargets = new Set(edges.flatMap(edge =>
1042
+ edge.sourceCluster.id === hubCluster.id
1043
+ ? [edge.targetCluster.id]
1044
+ : edge.targetCluster.id === hubCluster.id
1045
+ ? [edge.sourceCluster.id]
1046
+ : []
1047
+ ))
1048
+ const syntheticHubEdges = clusters
1049
+ .filter(cluster => cluster.id !== hubCluster.id && !existingHubTargets.has(cluster.id))
1050
+ .slice(0, ecosystemHubEdgeLimit)
1051
+ .map(cluster => ({
1052
+ id: hubCluster.id + ':' + cluster.id,
1053
+ sourceCluster: hubCluster,
1054
+ targetCluster: cluster,
1055
+ weight: 1,
1056
+ inferred: true
1057
+ }))
1058
+ return edges.concat(syntheticHubEdges)
935
1059
  }
936
1060
 
937
1061
  const edgeBudgetForCurrentFrame = () => {
@@ -2372,6 +2496,9 @@ const clusterRadiusPx = cluster => {
2372
2496
  if (cluster.id === 'macro-galaxy') {
2373
2497
  return 10
2374
2498
  }
2499
+ if (cluster.isHub) {
2500
+ return 5.2
2501
+ }
2375
2502
  if (String(cluster.id).startsWith('ecosystem-')) {
2376
2503
  const base = cluster.size >= 1000 ? 2.4 : cluster.size >= 250 ? 2.1 : 1.8
2377
2504
  return Math.max(1.8, Math.min(4.2, base + Math.log10(cluster.count + 1) * 0.28))
@@ -2807,10 +2934,11 @@ const render = now => {
2807
2934
  state.renderClusters.forEach(cluster => {
2808
2935
  const isMacro = cluster.id === 'macro-galaxy'
2809
2936
  const isEcosystem = String(cluster.id).startsWith('ecosystem-')
2937
+ const isHub = Boolean(cluster.isHub)
2810
2938
  const radiusPx = clusterRadiusPx(cluster)
2811
2939
  const radius = radiusPx / safeScale
2812
- const haloRadius = (radiusPx + (isMacro ? 8 : isEcosystem ? 1.1 : 4)) / safeScale
2813
- if (!isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
2940
+ const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
2941
+ if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
2814
2942
  ctx.beginPath()
2815
2943
  ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
2816
2944
  ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
@@ -2820,7 +2948,7 @@ const render = now => {
2820
2948
  ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
2821
2949
  ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
2822
2950
  ctx.fill()
2823
- ctx.lineWidth = (isEcosystem ? 0.7 : 1.4) / safeScale
2951
+ ctx.lineWidth = (isEcosystem && !isHub ? 0.7 : 1.4) / safeScale
2824
2952
  ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
2825
2953
  ctx.stroke()
2826
2954
  if (isMacro && cluster.representative?.title) {
@@ -3035,11 +3163,11 @@ const bindEvents = () => {
3035
3163
  })
3036
3164
  elements.zoomIn.addEventListener('click', () => {
3037
3165
  const rect = canvas.getBoundingClientRect()
3038
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14, 'button')
3166
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.055, 'button')
3039
3167
  })
3040
3168
  elements.zoomOut.addEventListener('click', () => {
3041
3169
  const rect = canvas.getBoundingClientRect()
3042
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88, 'button')
3170
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.948, 'button')
3043
3171
  })
3044
3172
  if (elements.fit) {
3045
3173
  elements.fit.addEventListener('click', () => {
@@ -3070,7 +3198,7 @@ const bindEvents = () => {
3070
3198
  const rect = canvas.getBoundingClientRect()
3071
3199
  const cursorX = event.clientX - rect.left
3072
3200
  const cursorY = event.clientY - rect.top
3073
- zoomAtPoint(cursorX, cursorY, 1.12)
3201
+ zoomAtPoint(cursorX, cursorY, 1.055)
3074
3202
  })
3075
3203
  canvas.addEventListener('pointerdown', event => {
3076
3204
  const point = worldPoint(event)
@@ -3144,14 +3272,14 @@ const bindEvents = () => {
3144
3272
  if (event.key === '+' || event.key === '=') {
3145
3273
  event.preventDefault()
3146
3274
  const rect = canvas.getBoundingClientRect()
3147
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.12)
3275
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.05)
3148
3276
  return
3149
3277
  }
3150
3278
 
3151
3279
  if (event.key === '-' || event.key === '_') {
3152
3280
  event.preventDefault()
3153
3281
  const rect = canvas.getBoundingClientRect()
3154
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.89)
3282
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.952)
3155
3283
  return
3156
3284
  }
3157
3285
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.87",
3
+ "version": "0.1.0-beta.89",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",