pi-weave 0.3.2 → 0.3.4

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.
@@ -0,0 +1,70 @@
1
+ /** Graph force settings shared without loading the d3 simulation. */
2
+
3
+ /**
4
+ * The force constants, as one **mutable** record.
5
+ *
6
+ * Mutable because picking these numbers is an act of taste, not of
7
+ * derivation: the difference between "one hairball" and "legible groups" is a
8
+ * ratio between containment cohesion, charge and centre gravity that is far
9
+ * easier to *see* than to reason about. The settings sliders (see
10
+ * `client/graph/tuner.model.ts` and docs/weave-workspace.md §15.7) update this record
11
+ * and re-run the layout live, so a human can find the values by eye and they then
12
+ * get frozen back into {@link FORCE_DEFAULTS}.
13
+ *
14
+ * `computeLayout(model, { seed })` stays deterministic for a given set of forces.
15
+ * Settings are the production writer of this record.
16
+ *
17
+ * ponytail: module-level mutable state, fine at one graph per page — thread it
18
+ * as a `LayoutOptions` field if a second concurrent consumer ever appears.
19
+ */
20
+ export interface ForceConstants {
21
+ /** Rest length of a `contains` / `anchored-at` spring: a rosette's radius. */
22
+ containsRest: number;
23
+ /** Stiffness of that spring. This is what makes a group *be* a group. */
24
+ containsStrength: number;
25
+ /** Rest length of a `links-to` / `mentions` spring: the inter-group reach. */
26
+ relationDistance: number;
27
+ /** Stiffness of that spring. Weak, so associations bend without merging. */
28
+ relationStrength: number;
29
+ /** `forceManyBody` strength. Negative is repulsion. */
30
+ charge: number;
31
+ /**
32
+ * Distance past which charge is ignored (`forceManyBody.distanceMax`).
33
+ *
34
+ * A cap rather than `Infinity` is what keeps a strong charge from simply
35
+ * inflating the whole graph uniformly: beyond it, neighbours stop pushing
36
+ * and only the springs and centre gravity speak, so groups separate at
37
+ * local scale without the picture growing without bound.
38
+ */
39
+ chargeMax: number;
40
+ /** `forceX`/`forceY` pull toward the origin. The no-escape guarantee. */
41
+ center: number;
42
+ }
43
+
44
+ /**
45
+ * The live constants. See {@link ForceConstants} for why this is mutable.
46
+ *
47
+ * These values were **found by eye** through the `?sliders=1` tuner and then
48
+ * frozen here, which is the loop §15.7 describes working as intended. They are
49
+ * not derived and should not be "corrected" toward rounder numbers: the
50
+ * previous set (`containsStrength: 0.02`, `charge: -50`, `center: 0.09`) was
51
+ * defensible on paper and produced a single hairball on screen.
52
+ */
53
+ export const FORCES: ForceConstants = {
54
+ containsRest: 55,
55
+ containsStrength: 0.12,
56
+ relationDistance: 50,
57
+ relationStrength: 0.07,
58
+ charge: -200,
59
+ chargeMax: 800,
60
+ center: 0.05,
61
+ };
62
+
63
+ /** The shipped values, for the tuner's reset and for a test to restore from. */
64
+ export const FORCE_DEFAULTS: Readonly<ForceConstants> = { ...FORCES };
65
+
66
+ /** Overwrite the live constants in place. The tuner's one write. */
67
+ export function setForces(next: Partial<ForceConstants>): void {
68
+ Object.assign(FORCES, next);
69
+ }
70
+
@@ -4,6 +4,10 @@ import { forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY
4
4
  import type { Simulation, SimulationNodeDatum } from "d3-force";
5
5
  import type { WireEdgeKind as EdgeKind, WireGraphEdge as GraphEdge, WireGraphModel as GraphModel } from "./graph";
6
6
  import type { Point } from "./metrics";
7
+ import { FORCES } from "./forces";
8
+
9
+ export { FORCES, FORCE_DEFAULTS, setForces } from "./forces";
10
+ export type { ForceConstants } from "./forces";
7
11
 
8
12
  export type { Point } from "./metrics";
9
13
 
@@ -35,74 +39,6 @@ const DEFAULT_TICKS = 300;
35
39
  const DEFAULT_SEED = 1;
36
40
  const ALPHA_MIN = 0.001;
37
41
 
38
- /**
39
- * The force constants, as one **mutable** record.
40
- *
41
- * Mutable because picking these numbers is an act of taste, not of
42
- * derivation: the difference between "one hairball" and "legible groups" is a
43
- * ratio between containment cohesion, charge and centre gravity that is far
44
- * easier to *see* than to reason about. The settings sliders (see
45
- * `client/graph/tuner.model.ts` and docs/weave-workspace.md §15.7) update this record
46
- * and re-run the layout live, so a human can find the values by eye and they then
47
- * get frozen back into {@link FORCE_DEFAULTS}.
48
- *
49
- * `computeLayout(model, { seed })` stays deterministic for a given set of forces.
50
- * Settings are the production writer of this record.
51
- *
52
- * ponytail: module-level mutable state, fine at one graph per page — thread it
53
- * as a `LayoutOptions` field if a second concurrent consumer ever appears.
54
- */
55
- export interface ForceConstants {
56
- /** Rest length of a `contains` / `anchored-at` spring: a rosette's radius. */
57
- containsRest: number;
58
- /** Stiffness of that spring. This is what makes a group *be* a group. */
59
- containsStrength: number;
60
- /** Rest length of a `links-to` / `mentions` spring: the inter-group reach. */
61
- relationDistance: number;
62
- /** Stiffness of that spring. Weak, so associations bend without merging. */
63
- relationStrength: number;
64
- /** `forceManyBody` strength. Negative is repulsion. */
65
- charge: number;
66
- /**
67
- * Distance past which charge is ignored (`forceManyBody.distanceMax`).
68
- *
69
- * A cap rather than `Infinity` is what keeps a strong charge from simply
70
- * inflating the whole graph uniformly: beyond it, neighbours stop pushing
71
- * and only the springs and centre gravity speak, so groups separate at
72
- * local scale without the picture growing without bound.
73
- */
74
- chargeMax: number;
75
- /** `forceX`/`forceY` pull toward the origin. The no-escape guarantee. */
76
- center: number;
77
- }
78
-
79
- /**
80
- * The live constants. See {@link ForceConstants} for why this is mutable.
81
- *
82
- * These values were **found by eye** through the `?sliders=1` tuner and then
83
- * frozen here, which is the loop §15.7 describes working as intended. They are
84
- * not derived and should not be "corrected" toward rounder numbers: the
85
- * previous set (`containsStrength: 0.02`, `charge: -50`, `center: 0.09`) was
86
- * defensible on paper and produced a single hairball on screen.
87
- */
88
- export const FORCES: ForceConstants = {
89
- containsRest: 55,
90
- containsStrength: 0.12,
91
- relationDistance: 50,
92
- relationStrength: 0.07,
93
- charge: -200,
94
- chargeMax: 800,
95
- center: 0.05,
96
- };
97
-
98
- /** The shipped values, for the tuner's reset and for a test to restore from. */
99
- export const FORCE_DEFAULTS: Readonly<ForceConstants> = { ...FORCES };
100
-
101
- /** Overwrite the live constants in place. The tuner's one write. */
102
- export function setForces(next: Partial<ForceConstants>): void {
103
- Object.assign(FORCES, next);
104
- }
105
-
106
42
  export function isContainment(kind: EdgeKind): boolean {
107
43
  return kind === "contains" || kind === "anchored-at";
108
44
  }
@@ -1,6 +1,6 @@
1
1
  /** Validated preferences, stored alongside the workspace's existing snapshots. */
2
- import { FORCE_DEFAULTS } from "./layout";
3
- import type { ForceConstants } from "./layout";
2
+ import { FORCE_DEFAULTS } from "./forces";
3
+ import type { ForceConstants } from "./forces";
4
4
  import { ACCENTS, THEMES, isThemeChoice } from "./themes";
5
5
  import type { AccentChoice, PaletteChoice } from "./themes";
6
6