@yggy-tech/graphraum 0.34.0 → 0.34.1

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.
Files changed (3) hide show
  1. package/README.md +11 -16
  2. package/dist/index.js +72 -43
  3. package/package.json +54 -55
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="./docs/src/assets/logo-dark.svg">
4
- <img src="./docs/src/assets/logo-light.svg" alt="graphraum logo" width="128" height="128">
4
+ <img src="./docs/src/assets/logo-light.svg" alt="graphraum" width="128" height="128">
5
5
  </picture>
6
6
  <br>
7
7
  <strong>graphraum</strong>
@@ -11,9 +11,9 @@
11
11
 
12
12
  # graphraum
13
13
 
14
- An opinionated WebGL engine for interactive node-edge graphs in 2D and 3D.
14
+ yggy's WebGL engine for interactive node-edge graphs in 2D and 3D.
15
15
 
16
- graphraum is deliberately narrow: it renders graphs, accepts positions from any layout engine, and keeps the hot path outside a component framework. Nodes use one instanced mesh, edges use one batched geometry, and rendering happens on demand.
16
+ graphraum renders graphs, accepts positions from any layout engine, and keeps the hot path outside a component framework. Nodes use one instanced mesh, edges use one batched geometry, and rendering happens on demand.
17
17
 
18
18
  > Pre-1.0 development. The API will continue to evolve while the rendering and visual contracts are proven.
19
19
 
@@ -47,25 +47,20 @@ Repository decisions and agent instructions live in [`AGENTS.md`](./AGENTS.md).
47
47
 
48
48
  ## Install
49
49
 
50
- graphraum is currently published to the GitHub Packages npm registry. Configure the `@domudev` scope and authenticate with a classic GitHub token that has `read:packages` permission:
51
-
52
- ```ini
53
- # .npmrc
54
- @domudev:registry=https://npm.pkg.github.com
55
- //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
56
- ```
57
-
58
- Then install it with Bun:
59
-
60
50
  ```sh
61
- bun add @domudev/graphraum
51
+ npm install @yggy-tech/graphraum
52
+ yarn add @yggy-tech/graphraum
53
+ pnpm add @yggy-tech/graphraum
54
+ bun add @yggy-tech/graphraum
62
55
  ```
63
56
 
64
- Publishing is automated with semantic-release after CI succeeds on `main`. Commit types determine the next version, semantic-release creates the `v<version>` tag and GitHub Release, and the package is published to GitHub Packages. Do not manually edit the package version or create routine release tags.
57
+ Use the line for your package manager.
58
+
59
+ Publishing is automated with semantic-release after CI succeeds on `main`. Commit types determine the next version, semantic-release creates the `v<version>` tag and GitHub Release, and the package is published to npm. Do not manually edit the package version or create routine release tags.
65
60
 
66
61
  ## Documentation
67
62
 
68
- The [documentation](https://domudev.github.io/graphraum/), [API reference](https://domudev.github.io/graphraum/reference/api/), and the live [playground](https://domudev.github.io/graphraum/playground/) with the Prove benchmark are published on GitHub Pages. Run them locally with:
63
+ The [documentation](https://yggy-tech.github.io/graphraum/), [API reference](https://yggy-tech.github.io/graphraum/reference/api/), and the live [playground](https://yggy-tech.github.io/graphraum/playground/) with the Prove benchmark are published on GitHub Pages. Run them locally with:
69
64
 
70
65
  ```sh
71
66
  bun install --cwd docs
package/dist/index.js CHANGED
@@ -124,6 +124,14 @@ function outlineRadius(outline, headingX, headingY) {
124
124
  return low;
125
125
  }
126
126
  /**
127
+ * How far an edge end stops short of a 3D node's center, in world units: the outline radius (stroke
128
+ * included) less `clearance`. The renderer applies it on screen, where the camera-facing outline
129
+ * is, so it is exact for circles from every view. Other shapes use their radius along +x.
130
+ */
131
+ function outlineReach(outline, clearance = DEFAULT_ENDPOINT_CLEARANCE) {
132
+ return Math.max(0, outlineRadius(outline, 1, 0) - Math.max(clearance, 0));
133
+ }
134
+ /**
127
135
  * Walk from the node center toward the far point and stop on the outline,
128
136
  * then pull back by `clearance` so the stroke is not stabbed.
129
137
  *
@@ -1333,34 +1341,40 @@ function packEdgeInstances(input) {
1333
1341
  let x2 = input.endpointPositions[base + 3] ?? 0;
1334
1342
  let y2 = input.endpointPositions[base + 4] ?? 0;
1335
1343
  let z2 = input.endpointPositions[base + 5] ?? 0;
1344
+ let trimSource = 0;
1345
+ let trimTarget = 0;
1336
1346
  if (attach === "boundary" && input.nodeOutlines && input.edgeNodeIndices) {
1337
1347
  const sourceIndex = input.edgeNodeIndices[edgeIndex * 2] ?? 0;
1338
1348
  const targetIndex = input.edgeNodeIndices[edgeIndex * 2 + 1] ?? 0;
1339
1349
  const sourceOutline = input.nodeOutlines[sourceIndex] ?? {};
1340
1350
  const targetOutline = input.nodeOutlines[targetIndex] ?? {};
1341
- const trimmed = trimEdgeEndpoints({
1342
- attach,
1343
- clearance,
1344
- mode: input.mode,
1345
- source: {
1346
- x: x1,
1347
- y: y1,
1348
- z: z1
1349
- },
1350
- target: {
1351
- x: x2,
1352
- y: y2,
1353
- z: z2
1354
- },
1355
- sourceOutline,
1356
- targetOutline
1357
- });
1358
- x1 = trimmed.source.x;
1359
- y1 = trimmed.source.y;
1360
- z1 = trimmed.source.z ?? 0;
1361
- x2 = trimmed.target.x;
1362
- y2 = trimmed.target.y;
1363
- z2 = trimmed.target.z ?? 0;
1351
+ if (input.mode === "3d") {
1352
+ trimSource = outlineReach(sourceOutline, clearance);
1353
+ trimTarget = outlineReach(targetOutline, clearance);
1354
+ } else {
1355
+ const trimmed = trimEdgeEndpoints({
1356
+ attach,
1357
+ clearance,
1358
+ source: {
1359
+ x: x1,
1360
+ y: y1,
1361
+ z: z1
1362
+ },
1363
+ target: {
1364
+ x: x2,
1365
+ y: y2,
1366
+ z: z2
1367
+ },
1368
+ sourceOutline,
1369
+ targetOutline
1370
+ });
1371
+ x1 = trimmed.source.x;
1372
+ y1 = trimmed.source.y;
1373
+ z1 = trimmed.source.z ?? 0;
1374
+ x2 = trimmed.target.x;
1375
+ y2 = trimmed.target.y;
1376
+ z2 = trimmed.target.z ?? 0;
1377
+ }
1364
1378
  }
1365
1379
  const { color, opacity } = resolveEdgePaint(edgeIndex, visual, input.defaults, input.tier, input.edgeStates);
1366
1380
  const width = input.tier === "overview" ? input.defaults.width : visual.width ?? input.defaults.width;
@@ -1396,7 +1410,9 @@ function packEdgeInstances(input) {
1396
1410
  color,
1397
1411
  width,
1398
1412
  opacity,
1399
- style
1413
+ style,
1414
+ trimStart: index === 0 ? trimSource : 0,
1415
+ trimEnd: index === segmentSpans - 1 ? trimTarget : 0
1400
1416
  });
1401
1417
  }
1402
1418
  if (input.tier === "overview") continue;
@@ -1421,7 +1437,8 @@ function packEdgeInstances(input) {
1421
1437
  dz: fromSource ? (first.z ?? 0) - (second.z ?? 0) : (last.z ?? 0) - (previous.z ?? 0),
1422
1438
  color,
1423
1439
  size: markerSize,
1424
- opacity
1440
+ opacity,
1441
+ trim: fromSource ? trimSource : trimTarget
1425
1442
  });
1426
1443
  };
1427
1444
  if (ends === "source" || ends === "both") add("source");
@@ -1494,6 +1511,7 @@ attribute vec3 instanceEndB;
1494
1511
  attribute vec4 instanceColor;
1495
1512
  attribute float instanceWidth;
1496
1513
  attribute float instanceStyle;
1514
+ attribute vec2 instanceTrim;
1497
1515
 
1498
1516
  varying vec4 edgeColor;
1499
1517
  varying float edgeKind;
@@ -1517,6 +1535,18 @@ void main() {
1517
1535
  vec2 delta = viewB.xy - viewA.xy;
1518
1536
  float len = length(delta);
1519
1537
  vec2 dir = len > 0.0001 ? delta / len : vec2(1.0, 0.0);
1538
+ // 3D boundary ends (see trimViewSegment): stop on the camera-facing outline. A trimmed end
1539
+ // keeps its node's depth, where the billboard is, so it meets the rim from every view.
1540
+ if (instanceTrim.x + instanceTrim.y > 0.0) {
1541
+ if (len - instanceTrim.x - instanceTrim.y <= 0.0) {
1542
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0); // hidden behind its nodes: clip it away
1543
+ return;
1544
+ }
1545
+ viewA.xy += dir * instanceTrim.x;
1546
+ viewB.xy -= dir * instanceTrim.y;
1547
+ delta = viewB.xy - viewA.xy;
1548
+ len = length(delta);
1549
+ }
1520
1550
  vec2 perp = vec2(-dir.y, dir.x);
1521
1551
  vec2 mid = (viewA.xy + viewB.xy) * 0.5;
1522
1552
  vec2 offset = dir * position.x * len + perp * position.y * instanceWidth;
@@ -1531,8 +1561,10 @@ void main() {
1531
1561
  vec2 dir = dirLen > 0.0001 ? dirVec / dirLen : vec2(1.0, 0.0);
1532
1562
  vec2 perp = vec2(-dir.y, dir.x);
1533
1563
  float t = position.x + 0.5;
1564
+ // 3D boundary ends pull the apex back onto the camera-facing outline (see markerApexInView).
1565
+ vec2 apex = viewA.xy - dir * instanceTrim.x;
1534
1566
  vec2 offset = dir * instanceWidth * (t - 1.0) + perp * position.y * instanceWidth;
1535
- viewPosition = vec4(viewA.xy + offset, viewA.z, 1.0);
1567
+ viewPosition = vec4(apex + offset, viewA.z, 1.0);
1536
1568
  }
1537
1569
 
1538
1570
  gl_Position = projectionMatrix * viewPosition;
@@ -1565,6 +1597,16 @@ void main() {
1565
1597
  ${OUTPUT_COLOR_SPACE_FRAGMENT}
1566
1598
  }
1567
1599
  `;
1600
+ /** Per-instance attributes of the edge geometry; updates mark each of them dirty. */
1601
+ const EDGE_INSTANCE_ATTRIBUTES = [
1602
+ "instanceKind",
1603
+ "instanceEndA",
1604
+ "instanceEndB",
1605
+ "instanceColor",
1606
+ "instanceWidth",
1607
+ "instanceStyle",
1608
+ "instanceTrim"
1609
+ ];
1568
1610
  function createEdgeGeometry(capacity) {
1569
1611
  const geometry = new PlaneGeometry(1, 1);
1570
1612
  geometry.setAttribute("instanceKind", new InstancedBufferAttribute(new Float32Array(capacity), 1));
@@ -1573,6 +1615,7 @@ function createEdgeGeometry(capacity) {
1573
1615
  geometry.setAttribute("instanceColor", new InstancedBufferAttribute(new Float32Array(capacity * 4), 4));
1574
1616
  geometry.setAttribute("instanceWidth", new InstancedBufferAttribute(new Float32Array(capacity), 1));
1575
1617
  geometry.setAttribute("instanceStyle", new InstancedBufferAttribute(new Float32Array(capacity), 1));
1618
+ geometry.setAttribute("instanceTrim", new InstancedBufferAttribute(new Float32Array(capacity * 2), 2));
1576
1619
  return geometry;
1577
1620
  }
1578
1621
  function createEdgeMaterial() {
@@ -1602,6 +1645,7 @@ function writeEdgeSegmentInstance(geometry, slot, segment) {
1602
1645
  writeEdgeColorSlots(color, slot, 1, segment);
1603
1646
  width.setX(slot, segment.width);
1604
1647
  style.setX(slot, encodeEdgeStyle(segment.style));
1648
+ getInstancedAttribute(geometry, "instanceTrim").setXY(slot, segment.trimStart, segment.trimEnd);
1605
1649
  }
1606
1650
  function writeEdgeMarkerInstance(geometry, slot, marker) {
1607
1651
  const kind = getInstancedAttribute(geometry, "instanceKind");
@@ -1616,6 +1660,7 @@ function writeEdgeMarkerInstance(geometry, slot, marker) {
1616
1660
  writeEdgeColorSlots(color, slot, 1, marker);
1617
1661
  width.setX(slot, marker.size);
1618
1662
  style.setX(slot, 0);
1663
+ getInstancedAttribute(geometry, "instanceTrim").setXY(slot, marker.trim, 0);
1619
1664
  }
1620
1665
  /** Writes one color and opacity into `count` consecutive instance slots starting at `start`. */
1621
1666
  function writeEdgeColorSlots(color, start, count, paint) {
@@ -1624,14 +1669,6 @@ function writeEdgeColorSlots(color, start, count, paint) {
1624
1669
  }
1625
1670
  //#endregion
1626
1671
  //#region src/edge-viewport-patch.ts
1627
- const EDGE_INSTANCE_ATTRIBUTES$1 = [
1628
- "instanceKind",
1629
- "instanceEndA",
1630
- "instanceEndB",
1631
- "instanceColor",
1632
- "instanceWidth",
1633
- "instanceStyle"
1634
- ];
1635
1672
  /** Records GPU slot ranges for each visible edge after a full viewport materialize. */
1636
1673
  function buildVisibleEdgeLayouts(segments, markers, segmentInstanceCount) {
1637
1674
  const layouts = /* @__PURE__ */ new Map();
@@ -1719,7 +1756,7 @@ function patchVisibleEdgeInstances(geometry, input, pickableSegments) {
1719
1756
  ok: true,
1720
1757
  pickableSegments: nextPickable
1721
1758
  };
1722
- for (const name of EDGE_INSTANCE_ATTRIBUTES$1) {
1759
+ for (const name of EDGE_INSTANCE_ATTRIBUTES) {
1723
1760
  const attribute = geometry.getAttribute(name);
1724
1761
  attribute.addUpdateRange(dirtySlotStart * attribute.itemSize, (dirtySlotEnd - dirtySlotStart + 1) * attribute.itemSize);
1725
1762
  attribute.needsUpdate = true;
@@ -2725,14 +2762,6 @@ const FLIGHT_CANCEL_EVENTS = [
2725
2762
  "pointerdown",
2726
2763
  "wheel"
2727
2764
  ];
2728
- const EDGE_INSTANCE_ATTRIBUTES = [
2729
- "instanceKind",
2730
- "instanceEndA",
2731
- "instanceEndB",
2732
- "instanceColor",
2733
- "instanceWidth",
2734
- "instanceStyle"
2735
- ];
2736
2765
  /** A small, explicit WebGL graph renderer: one instanced node mesh and one batched edge geometry. */
2737
2766
  var Graphraum = class {
2738
2767
  container;
package/package.json CHANGED
@@ -1,57 +1,56 @@
1
1
  {
2
- "name": "@yggy-tech/graphraum",
3
- "version": "0.34.0",
4
- "description": "An opinionated WebGL engine for interactive graphs in 2D and 3D",
5
- "type": "module",
6
- "license": "Apache-2.0",
7
- "packageManager": "bun@1.3.1",
8
- "publishConfig": {
9
- "access": "public",
10
- "registry": "https://registry.npmjs.org"
11
- },
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/yggy-tech/graphraum.git"
15
- },
16
- "sideEffects": false,
17
- "files": [
18
- "dist"
19
- ],
20
- "exports": {
21
- ".": {
22
- "types": "./dist/index.d.ts",
23
- "import": "./dist/index.js"
24
- }
25
- },
26
- "scripts": {
27
- "build": "tsdown",
28
- "check": "biome check . && tsc --noEmit",
29
- "dev": "vite examples/benchmark",
30
- "docs:build": "bun run --cwd docs build",
31
- "docs:archive": "bun run docs/scripts/build-release-archives.ts",
32
- "docs:check": "bun run --cwd docs check",
33
- "docs:dev": "bun run --cwd docs dev",
34
- "docs:test": "bun run --cwd docs test",
35
- "prepack": "bun run build",
36
- "release": "semantic-release",
37
- "test": "vitest run src examples --exclude 'docs/**'"
38
- },
39
- "engines": {
40
- "bun": ">=1.2.0"
41
- },
42
- "dependencies": {
43
- "graphology": "^0.26.0",
44
- "three": "^0.185.1"
45
- },
46
- "devDependencies": {
47
- "@biomejs/biome": "^2.5.5",
48
- "@commitlint/cli": "^21.2.1",
49
- "@commitlint/config-conventional": "^21.2.0",
50
- "@types/three": "^0.185.1",
51
- "semantic-release": "^25.0.8",
52
- "tsdown": "^0.22.13",
53
- "typescript": "^6.0.3",
54
- "vite": "^8.1.5",
55
- "vitest": "^4.1.10"
56
- }
2
+ "name": "@yggy-tech/graphraum",
3
+ "version": "0.34.1",
4
+ "description": "yggy's WebGL engine for interactive graphs in 2D and 3D",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "packageManager": "bun@1.3.1",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/yggy-tech/graphraum.git"
14
+ },
15
+ "sideEffects": false,
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "check": "biome check . && tsc --noEmit",
28
+ "dev": "vite examples/benchmark",
29
+ "docs:build": "bun run --cwd docs build",
30
+ "docs:archive": "bun run docs/scripts/build-release-archives.ts",
31
+ "docs:check": "bun run --cwd docs check",
32
+ "docs:dev": "bun run --cwd docs dev",
33
+ "docs:test": "bun run --cwd docs test",
34
+ "prepack": "bun run build",
35
+ "release": "semantic-release",
36
+ "test": "vitest run src examples --exclude 'docs/**'"
37
+ },
38
+ "engines": {
39
+ "bun": ">=1.2.0"
40
+ },
41
+ "dependencies": {
42
+ "graphology": "^0.26.0",
43
+ "three": "^0.185.1"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "^2.5.5",
47
+ "@commitlint/cli": "^21.2.1",
48
+ "@commitlint/config-conventional": "^21.2.0",
49
+ "@types/three": "^0.185.1",
50
+ "semantic-release": "^25.0.8",
51
+ "tsdown": "^0.22.13",
52
+ "typescript": "^6.0.3",
53
+ "vite": "^8.1.5",
54
+ "vitest": "^4.1.10"
55
+ }
57
56
  }