@almadar/ui 5.118.0 → 5.120.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.
@@ -36626,11 +36626,11 @@ var init_DocumentViewer = __esm({
36626
36626
  exports.DocumentViewer.displayName = "DocumentViewer";
36627
36627
  }
36628
36628
  });
36629
- function measureLabelWidth(text) {
36629
+ function measureLabelWidth(text, fontFamily = "system-ui") {
36630
36630
  if (typeof document === "undefined") return text.length * 6;
36631
36631
  if (!labelMeasureCtx) labelMeasureCtx = document.createElement("canvas").getContext("2d");
36632
36632
  if (!labelMeasureCtx) return text.length * 6;
36633
- labelMeasureCtx.font = "12px system-ui";
36633
+ labelMeasureCtx.font = `12px ${fontFamily}`;
36634
36634
  return labelMeasureCtx.measureText(text).width + 4;
36635
36635
  }
36636
36636
  function getGroupColor(group, groups) {
@@ -36655,6 +36655,9 @@ function mulberry32(a) {
36655
36655
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
36656
36656
  };
36657
36657
  }
36658
+ function edgeKeyOf(a, b) {
36659
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
36660
+ }
36658
36661
  function resolveColor3(color, el) {
36659
36662
  const m = /^var\((--[^,)]+)(?:,\s*([^)]+))?\)$/.exec(color.trim());
36660
36663
  if (!m) return color;
@@ -36786,8 +36789,12 @@ var init_GraphCanvas = __esm({
36786
36789
  React73.useEffect(() => {
36787
36790
  const canvas = canvasRef.current;
36788
36791
  if (!canvas || propNodes.length === 0) return;
36789
- const w = logicalW;
36790
- const h = height;
36792
+ const viewW = logicalW;
36793
+ const viewH = height;
36794
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
36795
+ const w = viewW * layoutScale;
36796
+ const h = viewH * layoutScale;
36797
+ const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
36791
36798
  const prevPos = /* @__PURE__ */ new Map();
36792
36799
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
36793
36800
  const simNodes = propNodes.map((n, idx) => {
@@ -36822,133 +36829,192 @@ var init_GraphCanvas = __esm({
36822
36829
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
36823
36830
  if (layout === "force") {
36824
36831
  const maxIterations = 300;
36825
- const tick = () => {
36832
+ const COOL = 0.12;
36833
+ const COLLIDE_PASSES = 6;
36834
+ const LABEL_GAP = 12;
36835
+ const LABEL_H = 16;
36836
+ const SIMILARITY_NEIGHBORS = 5;
36837
+ const SIMILARITY_MIN_WEIGHT = 0.25;
36838
+ const drawnPairs = /* @__PURE__ */ new Set();
36839
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36840
+ const similarityNeighbors = /* @__PURE__ */ new Map();
36841
+ {
36842
+ const bySource = /* @__PURE__ */ new Map();
36843
+ for (const pair of propSimilarity) {
36844
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
36845
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
36846
+ const arr = bySource.get(pair.source) ?? [];
36847
+ arr.push(pair);
36848
+ bySource.set(pair.source, arr);
36849
+ }
36850
+ for (const [id, arr] of bySource) {
36851
+ arr.sort((a, b) => b.weight - a.weight);
36852
+ const keep = new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target));
36853
+ similarityNeighbors.set(id, keep);
36854
+ }
36855
+ }
36856
+ const activeSimilarity = propSimilarity.filter((pair) => {
36857
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
36858
+ const a = similarityNeighbors.get(pair.source);
36859
+ const b = similarityNeighbors.get(pair.target);
36860
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
36861
+ });
36862
+ const tick = (iter, options) => {
36863
+ const skipForces = options?.skipForces ?? false;
36826
36864
  const nodes = nodesRef.current;
36827
36865
  const centerX = w / 2;
36828
36866
  const centerY = h / 2;
36867
+ const temp = skipForces ? 0 : Math.max(COOL, 1 - iter / maxIterations);
36829
36868
  for (const node of nodes) {
36830
36869
  node.fx = 0;
36831
36870
  node.fy = 0;
36832
36871
  }
36833
- for (let i = 0; i < nodes.length; i++) {
36834
- for (let j = i + 1; j < nodes.length; j++) {
36835
- const dx = nodes[j].x - nodes[i].x;
36836
- const dy = nodes[j].y - nodes[i].y;
36837
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36838
- const force = repulsion / (dist * dist);
36839
- const fx = dx / dist * force;
36840
- const fy = dy / dist * force;
36841
- nodes[i].fx -= fx;
36842
- nodes[i].fy -= fy;
36843
- nodes[j].fx += fx;
36844
- nodes[j].fy += fy;
36872
+ if (!skipForces) {
36873
+ for (let i = 0; i < nodes.length; i++) {
36874
+ for (let j = i + 1; j < nodes.length; j++) {
36875
+ const dx = nodes[j].x - nodes[i].x;
36876
+ const dy = nodes[j].y - nodes[i].y;
36877
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36878
+ const force = repulsion / (dist * dist) * temp;
36879
+ const fx = dx / dist * force;
36880
+ const fy = dy / dist * force;
36881
+ nodes[i].fx -= fx;
36882
+ nodes[i].fy -= fy;
36883
+ nodes[j].fx += fx;
36884
+ nodes[j].fy += fy;
36885
+ }
36845
36886
  }
36846
- }
36847
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
36848
- if (propSimilarity.length > 0) {
36849
- for (const pair of propSimilarity) {
36850
- const source = nodeById.get(pair.source);
36851
- const target = nodeById.get(pair.target);
36852
- if (!source || !target) continue;
36853
- const dx = target.x - source.x;
36854
- const dy = target.y - source.y;
36887
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
36888
+ const spring = (a, b, rest, k) => {
36889
+ const dx = b.x - a.x;
36890
+ const dy = b.y - a.y;
36855
36891
  const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36856
- const w2 = Math.min(1, Math.max(0, pair.weight));
36857
- const linkTarget = linkDistance * (1 - w2);
36858
- const force = (dist - linkTarget) * (0.04 + 0.1 * w2);
36892
+ const force = (dist - rest) * k * temp;
36859
36893
  const fx = dx / dist * force;
36860
36894
  const fy = dy / dist * force;
36861
- source.fx += fx;
36862
- source.fy += fy;
36863
- target.fx -= fx;
36864
- target.fy -= fy;
36865
- }
36866
- } else {
36895
+ a.fx += fx;
36896
+ a.fy += fy;
36897
+ b.fx -= fx;
36898
+ b.fy -= fy;
36899
+ };
36867
36900
  for (const edge of propEdges) {
36868
36901
  const source = nodeById.get(edge.source);
36869
36902
  const target = nodeById.get(edge.target);
36870
36903
  if (!source || !target) continue;
36871
- const dx = target.x - source.x;
36872
- const dy = target.y - source.y;
36873
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36904
+ drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36874
36905
  const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
36875
- const linkTarget = linkDistance * (0.5 + (1 - w2) * 1.5);
36876
- const force = (dist - linkTarget) * (0.04 + 0.1 * w2);
36877
- const fx = dx / dist * force;
36878
- const fy = dy / dist * force;
36879
- source.fx += fx;
36880
- source.fy += fy;
36881
- target.fx -= fx;
36882
- target.fy -= fy;
36906
+ spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
36883
36907
  }
36884
- }
36885
- const centroids = /* @__PURE__ */ new Map();
36886
- for (const node of nodes) {
36887
- const g = node.group ?? "__none__";
36888
- let c = centroids.get(g);
36889
- if (!c) {
36890
- c = { x: 0, y: 0, n: 0 };
36891
- centroids.set(g, c);
36908
+ for (const pair of activeSimilarity) {
36909
+ const source = nodeById.get(pair.source);
36910
+ const target = nodeById.get(pair.target);
36911
+ if (!source || !target) continue;
36912
+ const w2 = Math.min(1, Math.max(0, pair.weight));
36913
+ spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
36892
36914
  }
36893
- c.x += node.x;
36894
- c.y += node.y;
36895
- c.n += 1;
36896
- }
36897
- for (const node of nodes) {
36898
- const c = centroids.get(node.group ?? "__none__");
36899
- if (c && c.n > 1) {
36900
- node.fx += (c.x / c.n - node.x) * 0.04;
36901
- node.fy += (c.y / c.n - node.y) * 0.04;
36902
- } else {
36903
- node.fx += (centerX - node.x) * 0.01;
36904
- node.fy += (centerY - node.y) * 0.01;
36915
+ const centroids = /* @__PURE__ */ new Map();
36916
+ for (const node of nodes) {
36917
+ const g = node.group ?? "__none__";
36918
+ let c = centroids.get(g);
36919
+ if (!c) {
36920
+ c = { x: 0, y: 0, n: 0 };
36921
+ centroids.set(g, c);
36922
+ }
36923
+ c.x += node.x;
36924
+ c.y += node.y;
36925
+ c.n += 1;
36926
+ }
36927
+ const multiCluster = centroids.size > 1;
36928
+ for (const node of nodes) {
36929
+ if (multiCluster) {
36930
+ const c = centroids.get(node.group ?? "__none__");
36931
+ if (c && c.n > 1) {
36932
+ node.fx += (c.x / c.n - node.x) * 0.06 * temp;
36933
+ node.fy += (c.y / c.n - node.y) * 0.06 * temp;
36934
+ } else {
36935
+ node.fx += (centerX - node.x) * 0.01 * temp;
36936
+ node.fy += (centerY - node.y) * 0.01 * temp;
36937
+ }
36938
+ } else {
36939
+ node.fx += (centerX - node.x) * 8e-3 * temp;
36940
+ node.fy += (centerY - node.y) * 8e-3 * temp;
36941
+ }
36942
+ }
36943
+ const damping = 0.9;
36944
+ for (const node of nodes) {
36945
+ node.vx = (node.vx + node.fx) * damping;
36946
+ node.vy = (node.vy + node.fy) * damping;
36947
+ node.x += node.vx;
36948
+ node.y += node.vy;
36905
36949
  }
36906
36950
  }
36907
- const damping = 0.9;
36908
36951
  for (const node of nodes) {
36909
- node.vx = (node.vx + node.fx) * damping;
36910
- node.vy = (node.vy + node.fy) * damping;
36911
- node.x += node.vx;
36912
- node.y += node.vy;
36913
36952
  node.x = Math.max(30, Math.min(w - 30, node.x));
36914
36953
  node.y = Math.max(30, Math.min(h - 30, node.y));
36915
36954
  }
36916
- const LABEL_GAP = 12;
36917
- const LABEL_H = 16;
36918
36955
  const pad = nodeSpacing / 2;
36919
- for (let i = 0; i < nodes.length; i++) {
36920
- for (let j = i + 1; j < nodes.length; j++) {
36921
- const a = nodes[i];
36922
- const b = nodes[j];
36923
- const ar = a.size || 8;
36924
- const br = b.size || 8;
36925
- if (showLabels) {
36926
- if (a.labelW === void 0) a.labelW = a.label ? measureLabelWidth(a.label) : 0;
36927
- if (b.labelW === void 0) b.labelW = b.label ? measureLabelWidth(b.label) : 0;
36928
- }
36929
- const labelBelow = showLabels ? LABEL_GAP + LABEL_H : 0;
36930
- const aHalfW = Math.max(ar, (a.labelW ?? 0) / 2) + pad;
36931
- const bHalfW = Math.max(br, (b.labelW ?? 0) / 2) + pad;
36932
- const aTop = a.y - ar - pad, aBot = a.y + ar + labelBelow + pad;
36933
- const bTop = b.y - br - pad, bBot = b.y + br + labelBelow + pad;
36934
- const overlapX = Math.min(a.x + aHalfW, b.x + bHalfW) - Math.max(a.x - aHalfW, b.x - bHalfW);
36935
- const overlapY = Math.min(aBot, bBot) - Math.max(aTop, bTop);
36936
- if (overlapX > 0 && overlapY > 0) {
36937
- if (overlapX <= overlapY) {
36938
- const push = overlapX / 2 * (b.x >= a.x ? 1 : -1);
36939
- a.x = Math.max(30, Math.min(w - 30, a.x - push));
36940
- b.x = Math.max(30, Math.min(w - 30, b.x + push));
36941
- } else {
36942
- const push = overlapY / 2 * (bTop + bBot >= aTop + aBot ? 1 : -1);
36943
- a.y = Math.max(30, Math.min(h - 30, a.y - push));
36944
- b.y = Math.max(30, Math.min(h - 30, b.y + push));
36956
+ const rectsOf = (n) => {
36957
+ const r = n.size || 8;
36958
+ const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
36959
+ return {
36960
+ circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
36961
+ label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
36962
+ };
36963
+ };
36964
+ const sep = (r1, r2) => {
36965
+ const ox = Math.min(r1[2], r2[2]) - Math.max(r1[0], r2[0]);
36966
+ const oy = Math.min(r1[3], r2[3]) - Math.max(r1[1], r2[1]);
36967
+ if (ox <= 0 || oy <= 0) return null;
36968
+ return ox <= oy ? { axis: "x", depth: ox, sign: r1[0] + r1[2] < r2[0] + r2[2] ? 1 : -1 } : { axis: "y", depth: oy, sign: r1[1] + r1[3] < r2[1] + r2[3] ? 1 : -1 };
36969
+ };
36970
+ for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
36971
+ for (let i = 0; i < nodes.length; i++) {
36972
+ for (let j = i + 1; j < nodes.length; j++) {
36973
+ const a = nodes[i];
36974
+ const b = nodes[j];
36975
+ const ra = rectsOf(a);
36976
+ const rb = rectsOf(b);
36977
+ let best = null;
36978
+ for (const [r1, r2] of [[ra.circle, rb.circle], [ra.circle, rb.label], [ra.label, rb.circle], [ra.label, rb.label]]) {
36979
+ const s = sep(r1, r2);
36980
+ if (s && (!best || s.depth < best.depth)) best = s;
36981
+ }
36982
+ if (best) {
36983
+ const push = best.depth / 2;
36984
+ if (best.axis === "x") {
36985
+ a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
36986
+ b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
36987
+ a.vx = 0;
36988
+ b.vx = 0;
36989
+ } else {
36990
+ a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
36991
+ b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
36992
+ a.vy = 0;
36993
+ b.vy = 0;
36994
+ }
36945
36995
  }
36946
36996
  }
36947
36997
  }
36948
36998
  }
36949
36999
  };
36950
37000
  if (fullRelayout) {
36951
- for (let i = 0; i < maxIterations; i++) tick();
37001
+ for (let i = 0; i < maxIterations; i++) tick(i);
37002
+ for (let i = 0; i < 4; i++) tick(maxIterations, { skipForces: true });
37003
+ const fitPad = 40;
37004
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
37005
+ for (const node of nodesRef.current) {
37006
+ const r = node.size || 8;
37007
+ const lw = showLabels ? node.labelW ?? (node.labelW = node.label ? measureLabelWidth(node.label, labelFont) : 0) : 0;
37008
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
37009
+ minY = Math.min(minY, node.y - r);
37010
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
37011
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
37012
+ }
37013
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
37014
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
37015
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
37016
+ setZoom(nextZoom);
37017
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
36952
37018
  laidOutRef.current = true;
36953
37019
  } else {
36954
37020
  const centroids = /* @__PURE__ */ new Map();
@@ -8832,10 +8832,11 @@ interface GraphEdge {
8832
8832
  color?: string;
8833
8833
  }
8834
8834
  /**
8835
- * All-pairs similarity (cosine 0–1) used ONLY for layout: ideal pair distance
8836
- * is derived from similarity (higher closer). Never drawn `edges` remain
8837
- * the only rendered links. When provided it supersedes edge-weight attraction;
8838
- * when omitted, edges drive layout as before.
8835
+ * All-pairs similarity (cosine 0–1) used ONLY for layout, never drawn — `edges`
8836
+ * remain the only rendered links. It is the SECONDARY macro-layout: pairs that are
8837
+ * NOT directly connected get a weak spring (higher cosine ⇒ closer) so clusters
8838
+ * arrange relative to each other, while drawn `edges` stay the primary tight
8839
+ * structure. When omitted, edges alone drive the layout.
8839
8840
  */
8840
8841
  interface GraphSimilarity {
8841
8842
  source: string;
@@ -8857,9 +8858,10 @@ interface GraphCanvasProps {
8857
8858
  /** Graph edges (the only rendered links) */
8858
8859
  edges?: readonly GraphEdge[];
8859
8860
  /**
8860
- * All-pairs similarity (cosine 0–1) used ONLY for layout: ideal pair
8861
- * distance = linkDistance × (1 cosine). Never drawn. When provided it
8862
- * supersedes edge-weight attraction; when omitted, edges drive layout.
8861
+ * All-pairs similarity (cosine 0–1) used ONLY for layout, never drawn. It is the
8862
+ * secondary macro-layout: non-connected pairs get a weak spring (higher cosine
8863
+ * closer) so clusters arrange relative to each other; drawn `edges` stay the
8864
+ * primary tight structure. When omitted, edges alone drive the layout.
8863
8865
  */
8864
8866
  similarity?: readonly GraphSimilarity[];
8865
8867
  /** Canvas height */
@@ -8832,10 +8832,11 @@ interface GraphEdge {
8832
8832
  color?: string;
8833
8833
  }
8834
8834
  /**
8835
- * All-pairs similarity (cosine 0–1) used ONLY for layout: ideal pair distance
8836
- * is derived from similarity (higher closer). Never drawn `edges` remain
8837
- * the only rendered links. When provided it supersedes edge-weight attraction;
8838
- * when omitted, edges drive layout as before.
8835
+ * All-pairs similarity (cosine 0–1) used ONLY for layout, never drawn — `edges`
8836
+ * remain the only rendered links. It is the SECONDARY macro-layout: pairs that are
8837
+ * NOT directly connected get a weak spring (higher cosine ⇒ closer) so clusters
8838
+ * arrange relative to each other, while drawn `edges` stay the primary tight
8839
+ * structure. When omitted, edges alone drive the layout.
8839
8840
  */
8840
8841
  interface GraphSimilarity {
8841
8842
  source: string;
@@ -8857,9 +8858,10 @@ interface GraphCanvasProps {
8857
8858
  /** Graph edges (the only rendered links) */
8858
8859
  edges?: readonly GraphEdge[];
8859
8860
  /**
8860
- * All-pairs similarity (cosine 0–1) used ONLY for layout: ideal pair
8861
- * distance = linkDistance × (1 cosine). Never drawn. When provided it
8862
- * supersedes edge-weight attraction; when omitted, edges drive layout.
8861
+ * All-pairs similarity (cosine 0–1) used ONLY for layout, never drawn. It is the
8862
+ * secondary macro-layout: non-connected pairs get a weak spring (higher cosine
8863
+ * closer) so clusters arrange relative to each other; drawn `edges` stay the
8864
+ * primary tight structure. When omitted, edges alone drive the layout.
8863
8865
  */
8864
8866
  similarity?: readonly GraphSimilarity[];
8865
8867
  /** Canvas height */