@world-engines/authoring-ui 0.1.0-alpha.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.
Files changed (89) hide show
  1. package/LICENSE +46 -0
  2. package/dist/DesktopAuthoringShell.d.ts +178 -0
  3. package/dist/DesktopAuthoringShell.js +235 -0
  4. package/dist/index.d.ts +23 -0
  5. package/dist/index.js +12 -0
  6. package/dist/ladybug/BrowserGraphCanvasAdapter.d.ts +12 -0
  7. package/dist/ladybug/BrowserGraphCanvasAdapter.js +8 -0
  8. package/dist/ladybug/GraphCanvas.d.ts +121 -0
  9. package/dist/ladybug/GraphCanvas.js +1439 -0
  10. package/dist/ladybug/GraphEdge.d.ts +26 -0
  11. package/dist/ladybug/GraphEdge.js +41 -0
  12. package/dist/ladybug/LadybugExplorer.d.ts +58 -0
  13. package/dist/ladybug/LadybugExplorer.js +83 -0
  14. package/dist/ladybug/LadybugSurfaces.d.ts +25 -0
  15. package/dist/ladybug/LadybugSurfaces.js +9 -0
  16. package/dist/ladybug/LadybugTable.d.ts +15 -0
  17. package/dist/ladybug/LadybugTable.js +26 -0
  18. package/dist/ladybug/entity-props.d.ts +98 -0
  19. package/dist/ladybug/entity-props.js +363 -0
  20. package/dist/ladybug/graph-edge-geometry.d.ts +19 -0
  21. package/dist/ladybug/graph-edge-geometry.js +78 -0
  22. package/dist/ladybug/graph-edge-mask.d.ts +21 -0
  23. package/dist/ladybug/graph-edge-mask.js +52 -0
  24. package/dist/ladybug/graph-groups.d.ts +47 -0
  25. package/dist/ladybug/graph-groups.js +245 -0
  26. package/dist/ladybug/graph-layout-contract.d.ts +34 -0
  27. package/dist/ladybug/graph-layout-contract.js +55 -0
  28. package/dist/ladybug/graph-layout-solver.d.ts +16 -0
  29. package/dist/ladybug/graph-layout-solver.js +96 -0
  30. package/dist/ladybug/graph-layout-wasm.d.ts +9 -0
  31. package/dist/ladybug/graph-layout-wasm.js +88 -0
  32. package/dist/ladybug/graph-layout-worker-protocol.d.ts +18 -0
  33. package/dist/ladybug/graph-layout-worker-protocol.js +1 -0
  34. package/dist/ladybug/graph-layout-worker.d.ts +1 -0
  35. package/dist/ladybug/graph-layout-worker.js +66 -0
  36. package/dist/ladybug/graph-layout.d.ts +2 -0
  37. package/dist/ladybug/graph-layout.js +1 -0
  38. package/dist/ladybug/graph-layout.wasm +0 -0
  39. package/dist/ladybug/graph-node-metrics.d.ts +33 -0
  40. package/dist/ladybug/graph-node-metrics.js +173 -0
  41. package/dist/ladybug/graph-physics.d.ts +49 -0
  42. package/dist/ladybug/graph-physics.js +266 -0
  43. package/dist/ladybug/graph-placement.d.ts +33 -0
  44. package/dist/ladybug/graph-placement.js +253 -0
  45. package/dist/ladybug/graph.css +530 -0
  46. package/dist/ladybug/graph.d.ts +18 -0
  47. package/dist/ladybug/graph.js +9 -0
  48. package/dist/ladybug/score-tags.d.ts +11 -0
  49. package/dist/ladybug/score-tags.js +1 -0
  50. package/dist/map/MapWorkspace.d.ts +14 -0
  51. package/dist/map/MapWorkspace.js +9 -0
  52. package/dist/score/ScoreRadar.d.ts +10 -0
  53. package/dist/score/ScoreRadar.js +66 -0
  54. package/dist/score/index.d.ts +3 -0
  55. package/dist/score/index.js +3 -0
  56. package/dist/score/radar-index.d.ts +3 -0
  57. package/dist/score/radar-index.js +2 -0
  58. package/dist/score/scenario-scoring-features.d.ts +90 -0
  59. package/dist/score/scenario-scoring-features.js +380 -0
  60. package/dist/score/scenario-structural-score.d.ts +76 -0
  61. package/dist/score/scenario-structural-score.js +211 -0
  62. package/dist/score/score-presentation.d.ts +51 -0
  63. package/dist/score/score-presentation.js +44 -0
  64. package/dist/score/score-radar.css +1 -0
  65. package/dist/sync/DownloadPanel.d.ts +42 -0
  66. package/dist/sync/DownloadPanel.js +22 -0
  67. package/dist/sync/SyncSidebar.d.ts +47 -0
  68. package/dist/sync/SyncSidebar.js +31 -0
  69. package/dist/sync/UploadPanel.d.ts +64 -0
  70. package/dist/sync/UploadPanel.js +36 -0
  71. package/dist/trigger/TriggerBlueprint.d.ts +37 -0
  72. package/dist/trigger/TriggerBlueprint.js +26 -0
  73. package/dist/trigger/tmw/TmwBlueprint.d.ts +71 -0
  74. package/dist/trigger/tmw/TmwBlueprint.js +206 -0
  75. package/dist/trigger/tmw/TmwFlowLane.d.ts +22 -0
  76. package/dist/trigger/tmw/TmwFlowLane.js +44 -0
  77. package/dist/trigger/tmw/TmwSourceLane.d.ts +17 -0
  78. package/dist/trigger/tmw/TmwSourceLane.js +38 -0
  79. package/dist/trigger/tmw/TmwTabs.d.ts +20 -0
  80. package/dist/trigger/tmw/TmwTabs.js +8 -0
  81. package/dist/trigger/tmw/TmwTraceLane.d.ts +9 -0
  82. package/dist/trigger/tmw/TmwTraceLane.js +6 -0
  83. package/dist/trigger/tmw/TriggerDebugTab.d.ts +9 -0
  84. package/dist/trigger/tmw/TriggerDebugTab.js +25 -0
  85. package/dist/trigger/tmw/translate.d.ts +3 -0
  86. package/dist/trigger/tmw/translate.js +2 -0
  87. package/dist/view/ViewWorkspace.d.ts +19 -0
  88. package/dist/view/ViewWorkspace.js +8 -0
  89. package/package.json +89 -0
@@ -0,0 +1,33 @@
1
+ /** Graph node 内容尺寸的单一参数 authority。 */
2
+ export declare const GRAPH_NODE_METRICS: Readonly<{
3
+ minDiameter: 48;
4
+ maxDiameter: number;
5
+ pixelsPerSqrtCharacter: 3;
6
+ maxContentLength: 4096;
7
+ maxTraversalDepth: 8;
8
+ maxVisitedContainers: 2048;
9
+ maxVisitedEntries: 4096;
10
+ }>;
11
+ export interface GraphNodeMetrics {
12
+ readonly contentLength: number;
13
+ readonly diameter: number;
14
+ readonly radius: number;
15
+ /** true 表示预算已截断遍历,contentLength 只是有界下限而非精确值。 */
16
+ readonly truncated: boolean;
17
+ /** true 表示视觉尺寸已到上限,继续增加正文不会再放大节点。 */
18
+ readonly saturated: boolean;
19
+ }
20
+ /** 外部预计算 metrics 也必须经过相同 48-72px authority,radius 始终由 diameter 派生。 */
21
+ export declare function constrainGraphNodeMetrics(metrics: GraphNodeMetrics): GraphNodeMetrics;
22
+ export interface GraphContentProp {
23
+ readonly value?: unknown;
24
+ }
25
+ /** 宿主只需投影作者可见的 name/props envelope,无需泄漏完整 EntityNode。 */
26
+ export interface GraphContentEntity {
27
+ readonly name?: string | readonly string[];
28
+ readonly props: Readonly<Record<string, GraphContentProp | undefined>>;
29
+ }
30
+ export type GraphNameAttrPredicate = (key: string) => boolean;
31
+ /** 只读取作者可见文本;宿主可注入自己的 canonical name-key 判定。 */
32
+ export declare function measureGraphNodeContent(entity: GraphContentEntity, isNameAttrKey?: GraphNameAttrPredicate): number;
33
+ export declare function graphNodeMetrics(entity: GraphContentEntity, isNameAttrKey?: GraphNameAttrPredicate): GraphNodeMetrics;
@@ -0,0 +1,173 @@
1
+ import { NODE_DIAMETER } from "./graph-placement.js";
2
+ /** Graph node 内容尺寸的单一参数 authority。 */
3
+ export const GRAPH_NODE_METRICS = Object.freeze({
4
+ minDiameter: NODE_DIAMETER,
5
+ maxDiameter: NODE_DIAMETER * 1.5,
6
+ pixelsPerSqrtCharacter: 3,
7
+ maxContentLength: 4_096,
8
+ maxTraversalDepth: 8,
9
+ maxVisitedContainers: 2_048,
10
+ maxVisitedEntries: 4_096
11
+ });
12
+ /** 外部预计算 metrics 也必须经过相同 48-72px authority,radius 始终由 diameter 派生。 */
13
+ export function constrainGraphNodeMetrics(metrics) {
14
+ const rawContentLength = Number.isFinite(metrics.contentLength)
15
+ ? Math.max(0, Math.floor(metrics.contentLength))
16
+ : 0;
17
+ const contentLength = Math.min(GRAPH_NODE_METRICS.maxContentLength, rawContentLength);
18
+ const requestedDiameter = Number.isFinite(metrics.diameter)
19
+ ? metrics.diameter
20
+ : GRAPH_NODE_METRICS.minDiameter;
21
+ const diameter = Math.min(GRAPH_NODE_METRICS.maxDiameter, Math.max(GRAPH_NODE_METRICS.minDiameter, requestedDiameter));
22
+ return Object.freeze({
23
+ contentLength,
24
+ diameter,
25
+ radius: diameter / 2,
26
+ truncated: metrics.truncated || rawContentLength > GRAPH_NODE_METRICS.maxContentLength,
27
+ saturated: metrics.saturated || diameter === GRAPH_NODE_METRICS.maxDiameter
28
+ });
29
+ }
30
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
31
+ const consumeEntry = (state) => {
32
+ if (state.visitedEntries >= GRAPH_NODE_METRICS.maxVisitedEntries) {
33
+ state.truncated = true;
34
+ return false;
35
+ }
36
+ state.visitedEntries += 1;
37
+ return true;
38
+ };
39
+ const countString = (text, state) => {
40
+ // 逐 code point 消费;到预算即停,避免 Array.from 对超长正文全量扫描与复制。
41
+ for (const _codePoint of text) {
42
+ if (state.length >= GRAPH_NODE_METRICS.maxContentLength) {
43
+ state.truncated = true;
44
+ return;
45
+ }
46
+ state.length += 1;
47
+ }
48
+ };
49
+ const countPropValue = (value, state, depth) => {
50
+ if (depth > GRAPH_NODE_METRICS.maxTraversalDepth) {
51
+ state.truncated = true;
52
+ return;
53
+ }
54
+ if (typeof value === "string") {
55
+ countString(value, state);
56
+ return;
57
+ }
58
+ if (value === null || typeof value !== "object")
59
+ return;
60
+ if (state.seen.has(value)) {
61
+ state.truncated = true;
62
+ return;
63
+ }
64
+ if (state.visitedContainers >= GRAPH_NODE_METRICS.maxVisitedContainers) {
65
+ state.truncated = true;
66
+ return;
67
+ }
68
+ state.seen.add(value);
69
+ state.visitedContainers += 1;
70
+ const record = value;
71
+ // 真实 prop value 的资源和引用对象只含结构 identity,不是作者正文。
72
+ if (hasOwn(record, "$ref") || hasOwn(record, "$asset"))
73
+ return;
74
+ if (hasOwn(record, "$rich")) {
75
+ const segments = record.$rich;
76
+ if (!Array.isArray(segments))
77
+ return;
78
+ for (let index = 0; index < segments.length; index += 1) {
79
+ if (!consumeEntry(state))
80
+ return;
81
+ const segment = segments[index];
82
+ if (segment !== null
83
+ && typeof segment === "object"
84
+ && hasOwn(segment, "text")
85
+ && typeof segment.text === "string") {
86
+ countString(segment.text, state);
87
+ }
88
+ if (state.truncated)
89
+ return;
90
+ }
91
+ return;
92
+ }
93
+ if (Array.isArray(value)) {
94
+ // prop array 的每项都是 prop;只读 value,忽略 title/states/trigger 等元数据。
95
+ for (let index = 0; index < value.length; index += 1) {
96
+ if (!consumeEntry(state))
97
+ return;
98
+ const prop = value[index];
99
+ if (prop !== null && typeof prop === "object" && hasOwn(prop, "value")) {
100
+ countPropValue(prop.value, state, depth + 1);
101
+ }
102
+ if (state.truncated)
103
+ return;
104
+ }
105
+ return;
106
+ }
107
+ // prop object 的 key 是作者字段名,不能按名字过滤。
108
+ for (const key in record) {
109
+ if (!hasOwn(record, key))
110
+ continue;
111
+ if (!consumeEntry(state))
112
+ return;
113
+ const prop = record[key];
114
+ if (prop !== null && typeof prop === "object" && hasOwn(prop, "value")) {
115
+ countPropValue(prop.value, state, depth + 1);
116
+ }
117
+ if (state.truncated)
118
+ return;
119
+ }
120
+ };
121
+ const measureGraphNodeContentState = (entity, isNameAttrKey) => {
122
+ const state = {
123
+ length: 0,
124
+ visitedContainers: 0,
125
+ visitedEntries: 0,
126
+ truncated: false,
127
+ seen: new WeakSet()
128
+ };
129
+ const rawNames = typeof entity.name === "string"
130
+ ? [entity.name]
131
+ : Array.isArray(entity.name)
132
+ ? entity.name
133
+ : [];
134
+ for (let index = 0; index < rawNames.length; index += 1) {
135
+ if (!consumeEntry(state))
136
+ return state;
137
+ const name = rawNames[index];
138
+ if (typeof name === "string")
139
+ countString(name, state);
140
+ if (state.truncated)
141
+ return state;
142
+ }
143
+ for (const key in entity.props) {
144
+ if (!hasOwn(entity.props, key))
145
+ continue;
146
+ if (!consumeEntry(state))
147
+ return state;
148
+ // canonical name 已在顶层计量;仅在旧节点缺顶层名称时消费 props.name。
149
+ if (isNameAttrKey(key) && rawNames.length > 0)
150
+ continue;
151
+ countPropValue(entity.props[key]?.value, state, 0);
152
+ if (state.truncated)
153
+ return state;
154
+ }
155
+ return state;
156
+ };
157
+ const defaultIsNameAttrKey = (key) => key === "name";
158
+ /** 只读取作者可见文本;宿主可注入自己的 canonical name-key 判定。 */
159
+ export function measureGraphNodeContent(entity, isNameAttrKey = defaultIsNameAttrKey) {
160
+ return measureGraphNodeContentState(entity, isNameAttrKey).length;
161
+ }
162
+ export function graphNodeMetrics(entity, isNameAttrKey = defaultIsNameAttrKey) {
163
+ const measured = measureGraphNodeContentState(entity, isNameAttrKey);
164
+ const diameter = Math.min(GRAPH_NODE_METRICS.maxDiameter, Math.max(GRAPH_NODE_METRICS.minDiameter, GRAPH_NODE_METRICS.minDiameter
165
+ + GRAPH_NODE_METRICS.pixelsPerSqrtCharacter * Math.sqrt(measured.length)));
166
+ return constrainGraphNodeMetrics({
167
+ contentLength: measured.length,
168
+ diameter,
169
+ radius: diameter / 2,
170
+ truncated: measured.truncated,
171
+ saturated: diameter === GRAPH_NODE_METRICS.maxDiameter
172
+ });
173
+ }
@@ -0,0 +1,49 @@
1
+ export declare const PHYSICS_NODE_RADIUS: number;
2
+ export declare const PHYSICS_MIN_DIST: number;
3
+ export declare const PHYSICS_GRID_SIZE: number;
4
+ export declare const PHYSICS_IDEAL_LEN: number;
5
+ export declare const PHYSICS_REPULSION_K = 60000;
6
+ export declare const PHYSICS_SPRING_K = 0.06;
7
+ export declare const PHYSICS_MAX_STEP: number;
8
+ export declare const PHYSICS_STEP_SCALE = 60;
9
+ export declare const PHYSICS_SLEEP_SPEED = 4;
10
+ export declare const PHYSICS_FLUSH_INTERVAL_MS = 400;
11
+ export declare const PHYSICS_MAX_DT_MS = 32;
12
+ export declare const PHYSICS_EPSILON = 0.001;
13
+ export declare const PHYSICS_MIN_DIST_SQ: number;
14
+ export declare const PHYSICS_CELL_SIZE: number;
15
+ export declare const PHYSICS_OVERLAP_PUSH_FRACTION = 0.5;
16
+ export declare const PHYSICS_COLLISION_GAP: number;
17
+ export declare const PHYSICS_SPRING_GAP: number;
18
+ export interface PhysicsBody {
19
+ readonly id: string;
20
+ x: number;
21
+ y: number;
22
+ vx: number;
23
+ vy: number;
24
+ readonly mass: number;
25
+ readonly radius?: number;
26
+ }
27
+ export interface PhysicsSeedNode {
28
+ readonly id: string;
29
+ readonly x: number;
30
+ readonly y: number;
31
+ readonly radius?: number;
32
+ }
33
+ export interface PhysicsEdge {
34
+ readonly from: string;
35
+ readonly to: string;
36
+ }
37
+ export declare const computeNodeMass: (nodeId: string, edges: ReadonlyArray<PhysicsEdge>) => number;
38
+ export declare const initBodies: (seeds: ReadonlyArray<PhysicsSeedNode>, edges: ReadonlyArray<PhysicsEdge>) => PhysicsBody[];
39
+ /** 与 editor 已验证的可变半径 spatial-grid force tick 同算法。 */
40
+ export declare const stepPhysics: (bodies: PhysicsBody[], edges: ReadonlyArray<PhysicsEdge>, dtSeconds: number) => void;
41
+ export interface GridPoint {
42
+ readonly x: number;
43
+ readonly y: number;
44
+ readonly radius?: number;
45
+ }
46
+ export declare const snapToGrid: (x: number, y: number) => GridPoint;
47
+ export declare const tryFindGridSlot: (target: GridPoint, others: ReadonlyArray<GridPoint>) => GridPoint | null;
48
+ export declare const findGridSlot: (target: GridPoint, others: ReadonlyArray<GridPoint>) => GridPoint;
49
+ export declare const snapNodesToGrid: (seeds: ReadonlyArray<PhysicsSeedNode>) => PhysicsSeedNode[];
@@ -0,0 +1,266 @@
1
+ import { NODE_DIAMETER, NODE_RADIUS, NODE_SPACING_RADIUS_MULTIPLIER, requiredNodeCenterDistance } from "./graph-placement.js";
2
+ export const PHYSICS_NODE_RADIUS = NODE_DIAMETER / 2;
3
+ export const PHYSICS_MIN_DIST = requiredNodeCenterDistance(PHYSICS_NODE_RADIUS, PHYSICS_NODE_RADIUS);
4
+ export const PHYSICS_GRID_SIZE = PHYSICS_NODE_RADIUS;
5
+ export const PHYSICS_IDEAL_LEN = PHYSICS_NODE_RADIUS * 5;
6
+ export const PHYSICS_REPULSION_K = 60000;
7
+ export const PHYSICS_SPRING_K = 0.06;
8
+ export const PHYSICS_MAX_STEP = PHYSICS_NODE_RADIUS;
9
+ export const PHYSICS_STEP_SCALE = 60;
10
+ export const PHYSICS_SLEEP_SPEED = 4;
11
+ export const PHYSICS_FLUSH_INTERVAL_MS = 400;
12
+ export const PHYSICS_MAX_DT_MS = 32;
13
+ export const PHYSICS_EPSILON = 0.001;
14
+ export const PHYSICS_MIN_DIST_SQ = PHYSICS_MIN_DIST * PHYSICS_MIN_DIST;
15
+ export const PHYSICS_CELL_SIZE = PHYSICS_MIN_DIST * 2;
16
+ export const PHYSICS_OVERLAP_PUSH_FRACTION = 0.5;
17
+ export const PHYSICS_COLLISION_GAP = NODE_RADIUS * NODE_SPACING_RADIUS_MULTIPLIER;
18
+ export const PHYSICS_SPRING_GAP = PHYSICS_IDEAL_LEN - NODE_DIAMETER;
19
+ const PHYSICS_GRID_SEARCH_MAX_RING = 40;
20
+ const PHYSICS_MAX_NEIGHBORS_PER_BODY = 48;
21
+ const PHYSICS_VELOCITY_RESPONSE = 10;
22
+ const PHYSICS_OVERLAP_CORRECTION_SPEED = 8;
23
+ const PHYSICS_OVERLAP_SLOP = 0.75;
24
+ const PHYSICS_TAU = Math.PI * 2;
25
+ const PHYSICS_GOLDEN_ANGLE = 2.399963;
26
+ const finiteRadius = (radius) => typeof radius === "number" && Number.isFinite(radius) && radius > 0
27
+ ? radius
28
+ : PHYSICS_NODE_RADIUS;
29
+ export const computeNodeMass = (nodeId, edges) => {
30
+ let degree = 0;
31
+ for (const edge of edges) {
32
+ if (edge.from === nodeId || edge.to === nodeId)
33
+ degree += 1;
34
+ }
35
+ return 1 + degree;
36
+ };
37
+ export const initBodies = (seeds, edges) => {
38
+ const degreeById = new Map();
39
+ for (const edge of edges) {
40
+ degreeById.set(edge.from, (degreeById.get(edge.from) ?? 0) + 1);
41
+ degreeById.set(edge.to, (degreeById.get(edge.to) ?? 0) + 1);
42
+ }
43
+ return seeds.map((seed) => ({
44
+ id: seed.id,
45
+ x: seed.x,
46
+ y: seed.y,
47
+ vx: 0,
48
+ vy: 0,
49
+ mass: 1 + (degreeById.get(seed.id) ?? 0),
50
+ radius: finiteRadius(seed.radius)
51
+ }));
52
+ };
53
+ const clampStep = (value) => {
54
+ if (value > PHYSICS_MAX_STEP)
55
+ return PHYSICS_MAX_STEP;
56
+ if (value < -PHYSICS_MAX_STEP)
57
+ return -PHYSICS_MAX_STEP;
58
+ return value;
59
+ };
60
+ const buildSpatialGrid = (bodies, cellSize) => {
61
+ const cells = new Map();
62
+ for (let index = 0; index < bodies.length; index += 1) {
63
+ const body = bodies[index];
64
+ const cellX = Math.floor(body.x / cellSize);
65
+ const cellY = Math.floor(body.y / cellSize);
66
+ let row = cells.get(cellX);
67
+ if (!row) {
68
+ row = new Map();
69
+ cells.set(cellX, row);
70
+ }
71
+ let cell = row.get(cellY);
72
+ if (!cell) {
73
+ cell = [];
74
+ row.set(cellY, cell);
75
+ }
76
+ cell.push(index);
77
+ }
78
+ return { cellSize, cells };
79
+ };
80
+ const forEachNeighbor = (grid, x, y, afterIndex, visit) => {
81
+ let accepted = 0;
82
+ const cellX = Math.floor(x / grid.cellSize);
83
+ const cellY = Math.floor(y / grid.cellSize);
84
+ for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
85
+ const row = grid.cells.get(cellX + offsetX);
86
+ if (!row)
87
+ continue;
88
+ for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
89
+ const cell = row.get(cellY + offsetY);
90
+ if (!cell)
91
+ continue;
92
+ let low = 0;
93
+ let high = cell.length;
94
+ while (low < high) {
95
+ const middle = low + Math.floor((high - low) / 2);
96
+ if (cell[middle] <= afterIndex)
97
+ low = middle + 1;
98
+ else
99
+ high = middle;
100
+ }
101
+ for (let cursor = low; cursor < cell.length; cursor += 1) {
102
+ visit(cell[cursor]);
103
+ accepted += 1;
104
+ if (accepted >= PHYSICS_MAX_NEIGHBORS_PER_BODY)
105
+ return;
106
+ }
107
+ }
108
+ }
109
+ };
110
+ /** 与 editor 已验证的可变半径 spatial-grid force tick 同算法。 */
111
+ export const stepPhysics = (bodies, edges, dtSeconds) => {
112
+ const size = bodies.length;
113
+ if (size === 0)
114
+ return;
115
+ const forceX = new Array(size).fill(0);
116
+ const forceY = new Array(size).fill(0);
117
+ const correctionVx = new Array(size).fill(0);
118
+ const correctionVy = new Array(size).fill(0);
119
+ const maxRadius = bodies.reduce((largest, body) => Math.max(largest, finiteRadius(body.radius)), PHYSICS_NODE_RADIUS);
120
+ const gridCellSize = Math.max(PHYSICS_CELL_SIZE, (maxRadius * 2 + PHYSICS_COLLISION_GAP) * 2);
121
+ const grid = buildSpatialGrid(bodies, gridCellSize);
122
+ for (let left = 0; left < size; left += 1) {
123
+ const a = bodies[left];
124
+ forEachNeighbor(grid, a.x, a.y, left, (right) => {
125
+ const b = bodies[right];
126
+ let dx = a.x - b.x;
127
+ let dy = a.y - b.y;
128
+ let distSq = dx * dx + dy * dy;
129
+ if (distSq < PHYSICS_EPSILON) {
130
+ const angle = (left * PHYSICS_GOLDEN_ANGLE + right) % PHYSICS_TAU;
131
+ dx = Math.cos(angle) * PHYSICS_EPSILON;
132
+ dy = Math.sin(angle) * PHYSICS_EPSILON;
133
+ distSq = PHYSICS_EPSILON * PHYSICS_EPSILON;
134
+ }
135
+ const distance = Math.sqrt(distSq);
136
+ const collisionDistance = requiredNodeCenterDistance(a.radius, b.radius);
137
+ if (distance < collisionDistance) {
138
+ const unitX = dx / distance;
139
+ const unitY = dy / distance;
140
+ const penetration = Math.max(0, collisionDistance - distance - PHYSICS_OVERLAP_SLOP);
141
+ const correction = penetration * PHYSICS_OVERLAP_CORRECTION_SPEED * 0.5;
142
+ correctionVx[left] += unitX * correction;
143
+ correctionVy[left] += unitY * correction;
144
+ correctionVx[right] -= unitX * correction;
145
+ correctionVy[right] -= unitY * correction;
146
+ return;
147
+ }
148
+ const interactionDistance = collisionDistance * 2;
149
+ if (distance >= interactionDistance)
150
+ return;
151
+ const force = (PHYSICS_REPULSION_K * a.mass * b.mass) / distSq;
152
+ const rangeRatio = (interactionDistance - distance) /
153
+ (interactionDistance - collisionDistance);
154
+ const taperedForce = force * rangeRatio * rangeRatio;
155
+ const unitX = dx / distance;
156
+ const unitY = dy / distance;
157
+ forceX[left] += unitX * taperedForce;
158
+ forceY[left] += unitY * taperedForce;
159
+ forceX[right] -= unitX * taperedForce;
160
+ forceY[right] -= unitY * taperedForce;
161
+ });
162
+ }
163
+ const indexById = new Map();
164
+ for (let index = 0; index < size; index += 1)
165
+ indexById.set(bodies[index].id, index);
166
+ for (const edge of edges) {
167
+ const left = indexById.get(edge.from);
168
+ const right = indexById.get(edge.to);
169
+ if (left === undefined || right === undefined || left === right)
170
+ continue;
171
+ const a = bodies[left];
172
+ const b = bodies[right];
173
+ const dx = b.x - a.x;
174
+ const dy = b.y - a.y;
175
+ const distSq = dx * dx + dy * dy;
176
+ if (distSq < PHYSICS_EPSILON)
177
+ continue;
178
+ const distance = Math.sqrt(distSq);
179
+ const idealLength = requiredNodeCenterDistance(a.radius, b.radius);
180
+ const force = PHYSICS_SPRING_K * (distance - idealLength);
181
+ const unitX = dx / distance;
182
+ const unitY = dy / distance;
183
+ forceX[left] += unitX * force;
184
+ forceY[left] += unitY * force;
185
+ forceX[right] -= unitX * force;
186
+ forceY[right] -= unitY * force;
187
+ }
188
+ const safeDt = Math.max(0, dtSeconds);
189
+ const velocityBlend = 1 - Math.exp(-PHYSICS_VELOCITY_RESPONSE * safeDt);
190
+ for (let index = 0; index < size; index += 1) {
191
+ const body = bodies[index];
192
+ const accelerationX = forceX[index] / body.mass;
193
+ const accelerationY = forceY[index] / body.mass;
194
+ const targetVx = accelerationX * PHYSICS_STEP_SCALE + correctionVx[index];
195
+ const targetVy = accelerationY * PHYSICS_STEP_SCALE + correctionVy[index];
196
+ body.vx += (targetVx - body.vx) * velocityBlend;
197
+ body.vy += (targetVy - body.vy) * velocityBlend;
198
+ const targetSpeed = Math.hypot(targetVx, targetVy);
199
+ const speed = Math.hypot(body.vx, body.vy);
200
+ if (targetSpeed < PHYSICS_SLEEP_SPEED && speed < PHYSICS_SLEEP_SPEED) {
201
+ body.vx = 0;
202
+ body.vy = 0;
203
+ continue;
204
+ }
205
+ body.x += clampStep(body.vx * safeDt);
206
+ body.y += clampStep(body.vy * safeDt);
207
+ }
208
+ };
209
+ export const snapToGrid = (x, y) => ({
210
+ x: Math.round(x / PHYSICS_GRID_SIZE) * PHYSICS_GRID_SIZE,
211
+ y: Math.round(y / PHYSICS_GRID_SIZE) * PHYSICS_GRID_SIZE
212
+ });
213
+ const conflictsWithOthers = (point, others) => {
214
+ for (const other of others) {
215
+ const dx = point.x - other.x;
216
+ const dy = point.y - other.y;
217
+ const minDistance = requiredNodeCenterDistance(point.radius, other.radius);
218
+ if (dx * dx + dy * dy < minDistance * minDistance)
219
+ return true;
220
+ }
221
+ return false;
222
+ };
223
+ export const tryFindGridSlot = (target, others) => {
224
+ const start = snapToGrid(target.x, target.y);
225
+ const footprint = (point) => ({ ...point, radius: finiteRadius(target.radius) });
226
+ if (!conflictsWithOthers(footprint(start), others))
227
+ return start;
228
+ for (let ring = 1; ring <= PHYSICS_GRID_SEARCH_MAX_RING; ring += 1) {
229
+ const offset = ring * PHYSICS_GRID_SIZE;
230
+ for (let index = -ring; index <= ring; index += 1) {
231
+ const shift = index * PHYSICS_GRID_SIZE;
232
+ const top = { x: start.x + shift, y: start.y - offset };
233
+ if (!conflictsWithOthers(footprint(top), others))
234
+ return top;
235
+ const bottom = { x: start.x + shift, y: start.y + offset };
236
+ if (!conflictsWithOthers(footprint(bottom), others))
237
+ return bottom;
238
+ }
239
+ for (let index = -ring + 1; index <= ring - 1; index += 1) {
240
+ const shift = index * PHYSICS_GRID_SIZE;
241
+ const left = { x: start.x - offset, y: start.y + shift };
242
+ if (!conflictsWithOthers(footprint(left), others))
243
+ return left;
244
+ const right = { x: start.x + offset, y: start.y + shift };
245
+ if (!conflictsWithOthers(footprint(right), others))
246
+ return right;
247
+ }
248
+ }
249
+ return null;
250
+ };
251
+ export const findGridSlot = (target, others) => tryFindGridSlot(target, others) ?? snapToGrid(target.x, target.y);
252
+ export const snapNodesToGrid = (seeds) => {
253
+ const placed = [];
254
+ const out = [];
255
+ for (const seed of seeds) {
256
+ const slot = findGridSlot({ x: seed.x, y: seed.y, ...(seed.radius === undefined ? {} : { radius: seed.radius }) }, placed);
257
+ placed.push({ ...slot, radius: finiteRadius(seed.radius) });
258
+ out.push({
259
+ id: seed.id,
260
+ x: slot.x,
261
+ y: slot.y,
262
+ ...(seed.radius === undefined ? {} : { radius: finiteRadius(seed.radius) })
263
+ });
264
+ }
265
+ return out;
266
+ };
@@ -0,0 +1,33 @@
1
+ export declare const NODE_DIAMETER = 48;
2
+ export declare const NODE_RADIUS: number;
3
+ export declare const NODE_SPACING_RADIUS_MULTIPLIER = 3;
4
+ export declare const PLACEMENT_GAP: number;
5
+ export declare const SAFE_DIST: number;
6
+ export declare const SAFE_ZONE_MULT: number;
7
+ export declare const MIN_NODE_DIST: number;
8
+ export interface PlacementPoint {
9
+ readonly x: number;
10
+ readonly y: number;
11
+ readonly radius?: number;
12
+ }
13
+ export interface ExistingNode {
14
+ readonly position: PlacementPoint;
15
+ readonly radius?: number;
16
+ }
17
+ /** 外框净空为较大节点半径的三倍。 */
18
+ export declare const requiredNodeCenterDistance: (leftRadius: number | undefined, rightRadius: number | undefined) => number;
19
+ export declare const placeEntityWithoutOverlap: (existing: Readonly<Record<string, ExistingNode>>, candidate: PlacementPoint, excludeId?: string, distThreshold?: number) => PlacementPoint;
20
+ export interface PlaceableNode {
21
+ readonly id: string;
22
+ readonly position: PlacementPoint;
23
+ readonly radius?: number;
24
+ }
25
+ export declare const resolveOverlaps: <T extends PlaceableNode>(nodes: ReadonlyArray<T>, distThreshold?: number) => T[];
26
+ export interface ForceNode {
27
+ readonly id: string;
28
+ readonly position: PlacementPoint;
29
+ readonly radius?: number;
30
+ }
31
+ export declare const clampToSafeDistance: (target: PlacementPoint, others: ReadonlyArray<PlacementPoint>, distThreshold?: number) => PlacementPoint;
32
+ /** 保留 editor 既有的静态 force API;共享 Canvas 的按钮布局使用 graph-physics。 */
33
+ export declare const forcePlacement: <T extends ForceNode>(nodes: ReadonlyArray<T>) => T[];