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.
- package/README.md +8 -42
- package/package.json +1 -1
- package/src/web/client/dist/app.js +29 -29
- package/src/web/shared/forces.ts +70 -0
- package/src/web/shared/layout.ts +4 -68
- package/src/web/shared/preferences.ts +2 -2
|
@@ -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
|
+
|
package/src/web/shared/layout.ts
CHANGED
|
@@ -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 "./
|
|
3
|
-
import type { ForceConstants } from "./
|
|
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
|
|