@terrafirmagreg-team/recipe 0.1.0 → 1.2.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 (240) hide show
  1. package/package.json +16 -16
  2. package/src/components/button-copy.tsx +1 -0
  3. package/src/components/cards/card/index.tsx +5 -3
  4. package/src/components/cards/card/scene.tsx +4 -2
  5. package/src/components/cards/card/strip.tsx +4 -2
  6. package/src/components/cards/card/text.tsx +2 -1
  7. package/src/components/cards/card/tips.tsx +1 -0
  8. package/src/components/cards/default-item.tsx +6 -2
  9. package/src/components/cards/glare-filter.tsx +1 -0
  10. package/src/components/cards/paged-card.tsx +1 -0
  11. package/src/components/cards/slot-hitbox.tsx +4 -2
  12. package/src/components/cards/strip-card/buttons.tsx +7 -2
  13. package/src/components/cards/strip-card/index.tsx +3 -1
  14. package/src/components/cards/strip-card/member-cell.tsx +4 -2
  15. package/src/components/cards/strip-card/sprite.tsx +1 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +34 -16
  18. package/src/components/tool-list.tsx +1 -0
  19. package/src/{lib/tree.ts → components/tree.tsx} +20 -12
  20. package/src/components/views/alloy/index.tsx +14 -7
  21. package/src/components/views/alloy/list.tsx +3 -2
  22. package/src/components/views/alloy/mix/index.tsx +1 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +7 -6
  24. package/src/components/views/alloy/mix/rows.tsx +6 -7
  25. package/src/components/views/alloy/mix/status.tsx +8 -7
  26. package/src/components/views/alloy/mix/total.tsx +13 -12
  27. package/src/components/views/alloy/range-bar.tsx +2 -1
  28. package/src/components/views/alloy/stack-bar.tsx +4 -2
  29. package/src/components/views/browser/axis-switcher.tsx +13 -12
  30. package/src/components/views/browser/card-grid.tsx +2 -1
  31. package/src/components/views/browser/favorite-button.tsx +2 -1
  32. package/src/components/views/browser/frame.tsx +3 -2
  33. package/src/components/views/browser/index.tsx +20 -2
  34. package/src/components/views/browser/item-panel/index.tsx +11 -9
  35. package/src/components/views/browser/item-panel/skeleton.tsx +1 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +5 -3
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +4 -2
  38. package/src/components/views/browser/item-panel-grid/index.tsx +4 -2
  39. package/src/components/views/browser/item-view/index.tsx +7 -12
  40. package/src/components/views/browser/item-view/tags.tsx +7 -5
  41. package/src/components/views/browser/item-view-header.tsx +10 -9
  42. package/src/components/views/browser/pane-tabs.tsx +1 -0
  43. package/src/components/views/browser/place-header.tsx +4 -3
  44. package/src/components/views/browser/query-controls.tsx +3 -1
  45. package/src/components/views/browser/recipe-item-header.tsx +5 -4
  46. package/src/components/views/browser/settings.tsx +4 -3
  47. package/src/components/views/browser/toolbar.tsx +3 -2
  48. package/src/components/views/browser/type-tabs.tsx +5 -3
  49. package/src/components/views/browser/type-view.tsx +7 -12
  50. package/src/components/views/browser/workstation-rail.tsx +4 -3
  51. package/src/components/views/calc/calculator/actions.tsx +4 -14
  52. package/src/components/views/calc/calculator/help.tsx +69 -28
  53. package/src/components/views/calc/calculator/index.tsx +34 -82
  54. package/src/components/views/calc/dot-overlay.tsx +1 -0
  55. package/src/components/views/calc/edge.tsx +134 -32
  56. package/src/components/views/calc/flow/index.tsx +92 -54
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +84 -6
  58. package/src/components/views/calc/flow/toolbar.tsx +9 -21
  59. package/src/components/views/calc/flow/view-menu.tsx +16 -120
  60. package/src/components/views/calc/member-picker.tsx +8 -6
  61. package/src/components/views/calc/node-card/actions.tsx +28 -106
  62. package/src/components/views/calc/node-card/badges.tsx +10 -28
  63. package/src/components/views/calc/node-card/chips.tsx +8 -40
  64. package/src/components/views/calc/node-card/far.tsx +13 -65
  65. package/src/components/views/calc/node-card/full.tsx +133 -83
  66. package/src/components/views/calc/node-card/index.tsx +1 -0
  67. package/src/components/views/calc/node-card/menu.tsx +93 -22
  68. package/src/components/views/calc/node-card/port-cells.tsx +186 -0
  69. package/src/components/views/calc/node-card/port-rails.tsx +251 -0
  70. package/src/components/views/calc/node-card/ports.tsx +86 -0
  71. package/src/components/views/calc/recipe-picker/body.tsx +17 -14
  72. package/src/components/views/calc/recipe-picker/footer.tsx +5 -3
  73. package/src/components/views/calc/recipe-picker/index.tsx +5 -0
  74. package/src/components/views/calc/sidebar.tsx +19 -10
  75. package/src/components/views/calc/trees/icon-picker.tsx +3 -2
  76. package/src/components/views/calc/trees/index.tsx +5 -4
  77. package/src/components/views/calc/trees/label.tsx +6 -4
  78. package/src/components/views/calc/trees/rows.tsx +6 -5
  79. package/src/components/views/forge/gui.tsx +2 -1
  80. package/src/components/views/forge/index.tsx +14 -7
  81. package/src/components/views/forge/list.tsx +3 -2
  82. package/src/components/views/forge/workbench/controls.tsx +13 -6
  83. package/src/components/views/forge/workbench/index.tsx +3 -2
  84. package/src/components/views/forge/workbench/panel.tsx +1 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +5 -4
  86. package/src/components/views/forge/workbench/status.tsx +6 -5
  87. package/src/components/views/forge/workbench/tape.tsx +5 -4
  88. package/src/hooks/alloy/use-fluid-colors.ts +12 -9
  89. package/src/hooks/alloy/use-mix-state.ts +3 -2
  90. package/src/hooks/calc/flow/use-engage.ts +129 -0
  91. package/src/hooks/calc/flow/use-handlers.ts +192 -0
  92. package/src/hooks/calc/flow/use-layout.ts +105 -0
  93. package/src/hooks/calc/flow/{mount.ts → use-mount.ts} +1 -0
  94. package/src/hooks/calc/flow/{nodes.ts → use-nodes.ts} +46 -37
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/{viewport.ts → use-viewport.ts} +13 -26
  97. package/src/hooks/calc/flow/{window.ts → use-window.ts} +1 -0
  98. package/src/hooks/calc/use-actions.ts +208 -229
  99. package/src/hooks/calc/use-amounts.ts +23 -60
  100. package/src/hooks/calc/use-data.ts +106 -192
  101. package/src/hooks/calc/use-defaults.ts +4 -2
  102. package/src/hooks/calc/use-far-mode.ts +1 -0
  103. package/src/hooks/calc/use-focus.ts +8 -7
  104. package/src/hooks/calc/use-maps.ts +1 -0
  105. package/src/hooks/calc/use-reset.ts +1 -0
  106. package/src/hooks/calc/use-resolver.ts +32 -196
  107. package/src/hooks/calc/use-session.ts +1 -0
  108. package/src/hooks/calc/use-share.ts +5 -2
  109. package/src/hooks/calc/use-storage.ts +5 -4
  110. package/src/hooks/calc/use-tier.ts +1 -0
  111. package/src/hooks/calc/use-view-mode.ts +1 -0
  112. package/src/hooks/calc/use-worker.ts +56 -121
  113. package/src/hooks/forge/use-anvil-gui.ts +4 -2
  114. package/src/hooks/forge/use-forge-state.ts +1 -0
  115. package/src/hooks/forge/use-ruler.ts +1 -0
  116. package/src/hooks/use-add-to-calc.ts +23 -19
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +1 -0
  119. package/src/hooks/use-favorites.ts +1 -0
  120. package/src/hooks/use-group-outlines.ts +1 -0
  121. package/src/hooks/use-hide-unused.ts +2 -1
  122. package/src/hooks/use-index.ts +6 -2
  123. package/src/hooks/use-item-feeds.ts +3 -1
  124. package/src/hooks/use-item-pane.ts +6 -4
  125. package/src/hooks/use-item-press.ts +27 -23
  126. package/src/hooks/use-item-tag-groups.ts +4 -2
  127. package/src/hooks/use-item-tooltips.ts +3 -1
  128. package/src/hooks/use-left-sidebar-tree.ts +11 -3
  129. package/src/hooks/use-mod-names.ts +4 -2
  130. package/src/hooks/use-mount-limit.ts +1 -0
  131. package/src/hooks/use-panel-cells.ts +3 -1
  132. package/src/hooks/use-panel-page.ts +9 -6
  133. package/src/hooks/use-panel-tabs.ts +2 -1
  134. package/src/hooks/use-query.ts +8 -6
  135. package/src/hooks/use-session.ts +6 -2
  136. package/src/hooks/use-sidebar-group.ts +3 -7
  137. package/src/hooks/use-tag-members.ts +3 -1
  138. package/src/hooks/use-tool-recipes.ts +1 -0
  139. package/src/hooks/use-type-filter.ts +1 -0
  140. package/src/lib/alloy/mix.ts +1 -0
  141. package/src/lib/alloy/parse.ts +1 -0
  142. package/src/lib/calc/defaults.ts +6 -4
  143. package/src/lib/calc/flow.ts +143 -107
  144. package/src/lib/calc/focus.ts +1 -0
  145. package/src/lib/calc/meta.ts +3 -1
  146. package/src/lib/calc/pack.ts +47 -154
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +6 -38
  149. package/src/lib/calc/route/buckets.ts +1 -0
  150. package/src/lib/calc/route/geometry.ts +13 -24
  151. package/src/lib/calc/route/index.ts +1 -0
  152. package/src/lib/calc/route/polyline.ts +3 -3
  153. package/src/lib/calc/route/router.ts +181 -57
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +27 -19
  156. package/src/lib/calc/storage/index.ts +2 -0
  157. package/src/lib/calc/storage/keys.ts +4 -8
  158. package/src/lib/calc/storage/store.ts +11 -36
  159. package/src/lib/calc/tree/index.ts +2 -16
  160. package/src/lib/calc/tree/materials.ts +1 -0
  161. package/src/lib/calc/tree/recipe.ts +1 -0
  162. package/src/lib/calc/tree/share.ts +86 -149
  163. package/src/lib/calc/worker/api.ts +3 -11
  164. package/src/lib/calc/worker/fetch.ts +1 -0
  165. package/src/lib/calc/worker/flush.ts +7 -20
  166. package/src/lib/calc/worker/index.ts +1 -1
  167. package/src/lib/calc/worker/protocol.ts +16 -13
  168. package/src/lib/calc/worker/resolve.ts +32 -84
  169. package/src/lib/calc/worker/state.ts +5 -6
  170. package/src/{components/cards/card/model.ts → lib/card-model.ts} +1 -0
  171. package/src/lib/client/recipe-categories.ts +11 -93
  172. package/src/lib/client/recipe.ts +13 -11
  173. package/src/lib/client-index/catalog.ts +33 -27
  174. package/src/lib/client-index/chrome.ts +30 -30
  175. package/src/lib/client-index/index.ts +1 -0
  176. package/src/lib/client-index/registry.ts +70 -59
  177. package/src/lib/client-index/rows.ts +63 -57
  178. package/src/lib/client-index/tooltips.ts +28 -34
  179. package/src/lib/config.ts +2 -1
  180. package/src/lib/data/category/dto.ts +4 -3
  181. package/src/lib/data/category/load.ts +9 -8
  182. package/src/lib/data/category/parse.ts +4 -3
  183. package/src/lib/data/dto.ts +4 -3
  184. package/src/lib/data/grid.ts +2 -1
  185. package/src/lib/data/parse/entry.ts +3 -2
  186. package/src/lib/data/parse/index.ts +1 -0
  187. package/src/lib/data/parse/layout.ts +5 -2
  188. package/src/lib/data/parse/slots.ts +3 -2
  189. package/src/lib/data/parse/widgets.ts +3 -2
  190. package/src/lib/data/scene.ts +1 -1
  191. package/src/lib/data/slot-io.ts +1 -0
  192. package/src/lib/dump.ts +4 -3
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +1 -0
  195. package/src/lib/fluid-outputs.ts +10 -12
  196. package/src/lib/forge/actions.ts +2 -1
  197. package/src/lib/forge/data.ts +2 -1
  198. package/src/lib/forge/solve.ts +1 -0
  199. package/src/lib/group.ts +1 -0
  200. package/src/lib/index-store.ts +15 -9
  201. package/src/lib/links.ts +15 -9
  202. package/src/lib/lookup.ts +10 -8
  203. package/src/lib/panel-outline.ts +1 -0
  204. package/src/lib/panel.ts +3 -2
  205. package/src/lib/preview.ts +12 -8
  206. package/src/lib/recipe-slots.ts +1 -0
  207. package/src/lib/search.ts +1 -0
  208. package/src/lib/sidebar.ts +1 -0
  209. package/src/lib/used-items.ts +1 -0
  210. package/src/locales/en_us.json +170 -226
  211. package/src/locales/ru_ru.json +170 -226
  212. package/src/locales.ts +1 -0
  213. package/src/types/alloy.ts +1 -0
  214. package/src/{hooks/calc/flow/props.ts → types/calc-flow-props.ts} +18 -14
  215. package/src/types/calc.ts +41 -44
  216. package/src/types/forge.ts +1 -0
  217. package/src/types/index.ts +4 -2
  218. package/styles/calc.css +308 -47
  219. package/src/components/cards/fit-card.tsx +0 -26
  220. package/src/components/views/calc/target.tsx +0 -271
  221. package/src/hooks/calc/flow/engage.ts +0 -89
  222. package/src/hooks/calc/flow/handlers.ts +0 -115
  223. package/src/hooks/calc/flow/layout.ts +0 -149
  224. package/src/hooks/calc/flow/pickers.ts +0 -50
  225. package/src/hooks/calc/use-calc-fill.ts +0 -91
  226. package/src/hooks/calc/use-density.ts +0 -28
  227. package/src/hooks/calc/use-derived.ts +0 -258
  228. package/src/hooks/calc/use-direction.ts +0 -23
  229. package/src/hooks/calc/use-edge-style.ts +0 -40
  230. package/src/hooks/calc/use-fill.ts +0 -213
  231. package/src/lib/alloy/alloy.test.ts +0 -186
  232. package/src/lib/calc/tree/build.ts +0 -196
  233. package/src/lib/calc/tree/context.ts +0 -42
  234. package/src/lib/calc/tree/goal.ts +0 -64
  235. package/src/lib/calc/tree/ideal.ts +0 -36
  236. package/src/lib/calc/tree/pick.ts +0 -68
  237. package/src/lib/calc/tree/tree.test.ts +0 -169
  238. package/src/lib/calc/worker/fill.ts +0 -123
  239. package/src/lib/client-index/cache.ts +0 -13
  240. package/src/lib/forge/forge.test.ts +0 -123
@@ -1,19 +1,12 @@
1
- import { hydrateRecipe } from '@terrafirmagreg-team/recipe/lib/data/parse';
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
2
  import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
3
3
  import { loadClientRecipe } from '@terrafirmagreg-team/recipe/lib/client/recipe';
4
- import { resolveClientTag } from '@terrafirmagreg-team/game/lib/dump/client/tags';
4
+ import { resolveClientTag } from '@terrafirmagreg-team/dump/lib/dump/client/tags';
5
5
  import { recipesForOutput } from '@terrafirmagreg-team/recipe/lib/lookup';
6
- import {
7
- calcAutoPick,
8
- recipeDivisor,
9
- toCalcRecipe,
10
- type AutoPickResult,
11
- type BuildContext,
12
- } from './tree';
6
+ import { recipeDivisor, toCalcRecipe } from './tree';
13
7
  import type { RecipeCompactIndex, RecipeCompactRow } from '@terrafirmagreg-team/recipe/types';
14
8
  import type {
15
9
  CalcKind,
16
- CalcNode,
17
10
  CalcRecipe,
18
11
  CalcRecipeSummary,
19
12
  TagResolution,
@@ -141,26 +134,6 @@ export function createSummaries(
141
134
  };
142
135
  }
143
136
 
144
- export type PickContext = Pick<
145
- BuildContext,
146
- 'recipeMap' | 'getRecipesForItem' | 'recipesResolved' | 'deadIds'
147
- >;
148
-
149
- export function pickAutoRecipe(
150
- node: CalcNode,
151
- selections: Record<string, string>,
152
- ancestors: ReadonlySet<string>,
153
- ctx: PickContext,
154
- ): AutoPickResult {
155
- const chosen = node.path
156
- ? (selections[node.path] ?? selections[node.materialId])
157
- : selections[node.materialId];
158
- if (chosen && node.availableRecipes.some((row) => row.recipeId === chosen)) {
159
- return { pick: chosen, wantRecipes: [], wantOut: [] };
160
- }
161
- return calcAutoPick(node.availableRecipes, node.materialId, ancestors, ctx);
162
- }
163
-
164
137
  export type Wanted = {
165
138
  recipes: Set<string>;
166
139
  outs: Set<string>;
@@ -175,7 +148,7 @@ export type FetchedSlices = {
175
148
  };
176
149
 
177
150
  export async function fetchSlices(
178
- dataset: string,
151
+ dataset: DatasetRef,
179
152
  catalog: RecipeCompactIndex,
180
153
  wanted: Wanted,
181
154
  ): Promise<FetchedSlices> {
@@ -208,13 +181,7 @@ export async function fetchSlices(
208
181
  dead.add(id);
209
182
  continue;
210
183
  }
211
- const category = catalog.categories?.find(
212
- (row) => row.id === body.category,
213
- );
214
- const hydrated = category?.layout
215
- ? hydrateRecipe(body, category.layout)
216
- : body;
217
- recipes.set(id, { calc: toCalcRecipe(hydrated), body: hydrated });
184
+ recipes.set(id, { calc: toCalcRecipe(body), body });
218
185
  }
219
186
  const outs = new Map(outRows);
220
187
  const tags = new Map<string, TagResolution>(
@@ -228,3 +195,4 @@ export async function fetchSlices(
228
195
  );
229
196
  return { recipes, outs, tags, dead };
230
197
  }
198
+
@@ -46,3 +46,4 @@ export function createRectBuckets(
46
46
  return [...seen];
47
47
  };
48
48
  }
49
+
@@ -1,6 +1,6 @@
1
1
  import type { Node } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
2
2
  import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
3
- import type { CalcDirection } from '@terrafirmagreg-team/recipe/lib/calc/flow';
3
+ import { portAnchorPoint } from '@terrafirmagreg-team/recipe/lib/calc/ports';
4
4
 
5
5
  export const CELL = 8;
6
6
 
@@ -9,6 +9,8 @@ export const PAD = 6;
9
9
  export type RouteNode = Pick<Node, 'id' | 'position'> & {
10
10
  width?: number;
11
11
  height?: number;
12
+ inputs?: number;
13
+ outputs?: number;
12
14
  };
13
15
 
14
16
  export type Rect = { x: number; y: number; w: number; h: number };
@@ -20,26 +22,17 @@ export const toRect = (node: RouteNode): Rect => ({
20
22
  h: node.height ?? 0,
21
23
  });
22
24
 
23
- export const sameRect = (a: Rect, b: Rect) =>
24
- a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
25
-
26
25
  export function portAnchor(
27
26
  node: RouteNode,
28
27
  side: 'src' | 'tgt',
29
- direction: CalcDirection,
28
+ index = 0,
30
29
  ): FlowPoint {
31
- const w = node.width ?? 0;
32
- const h = node.height ?? 0;
33
- if (direction === 'horizontal') {
34
- return {
35
- x: node.position.x + (side === 'src' ? -w / 2 : w / 2),
36
- y: node.position.y,
37
- };
38
- }
39
- return {
40
- x: node.position.x,
41
- y: node.position.y + (side === 'src' ? -h / 2 : h / 2),
42
- };
30
+ return portAnchorPoint(
31
+ node,
32
+ side,
33
+ index,
34
+ side === 'tgt' ? (node.inputs ?? 1) : (node.outputs ?? 1),
35
+ );
43
36
  }
44
37
 
45
38
  export const cellKey = (x: number, y: number) => (y + 32768) * 65536 + (x + 32768);
@@ -51,12 +44,8 @@ export function zRoute(
51
44
  b: FlowPoint,
52
45
  sa: FlowPoint,
53
46
  sb: FlowPoint,
54
- direction: CalcDirection,
55
47
  ): FlowPoint[] {
56
- if (direction === 'horizontal') {
57
- const mid = (sa.x + sb.x) / 2;
58
- return [a, sa, { x: mid, y: sa.y }, { x: mid, y: sb.y }, sb, b];
59
- }
60
- const mid = (sa.y + sb.y) / 2;
61
- return [a, sa, { x: sa.x, y: mid }, { x: sb.x, y: mid }, sb, b];
48
+ const mid = (sa.x + sb.x) / 2;
49
+ return [a, sa, { x: mid, y: sa.y }, { x: mid, y: sb.y }, sb, b];
62
50
  }
51
+
@@ -1,2 +1,3 @@
1
1
  export type { RouteNode } from './geometry';
2
2
  export { createCircuitRouter } from './router';
3
+
@@ -1,5 +1,4 @@
1
1
  import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
2
- import type { CalcDirection } from '@terrafirmagreg-team/recipe/lib/calc/flow';
3
2
  import { CELL } from './geometry';
4
3
 
5
4
  export function simplifyCells(
@@ -68,8 +67,8 @@ export function cellsToPolyline(
68
67
  return px;
69
68
  }
70
69
 
71
- export function orthoPoints(points: FlowPoint[], direction: CalcDirection): FlowPoint[] {
72
- let prevAxis: 'h' | 'v' = direction === 'horizontal' ? 'h' : 'v';
70
+ export function orthoPoints(points: FlowPoint[]): FlowPoint[] {
71
+ let prevAxis: 'h' | 'v' = 'h';
73
72
  const out: FlowPoint[] = [points[0]!];
74
73
  for (let i = 1; i < points.length; i += 1) {
75
74
  const prev = out[out.length - 1]!;
@@ -101,3 +100,4 @@ export function orthoPoints(points: FlowPoint[], direction: CalcDirection): Flow
101
100
  if (out.length > 1) clean.push(out[out.length - 1]!);
102
101
  return clean;
103
102
  }
103
+
@@ -1,7 +1,6 @@
1
1
  import PF from 'pathfinding';
2
2
  import type { Edge } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
3
3
  import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
4
- import type { CalcDirection } from '@terrafirmagreg-team/recipe/lib/calc/flow';
5
4
  import { createRectBuckets } from './buckets';
6
5
  import {
7
6
  CELL,
@@ -10,7 +9,6 @@ import {
10
9
  keyX,
11
10
  keyY,
12
11
  portAnchor,
13
- sameRect,
14
12
  toRect,
15
13
  zRoute,
16
14
  type Rect,
@@ -20,30 +18,144 @@ import { cellsToPolyline, orthoPoints, simplifyCells } from './polyline';
20
18
 
21
19
  const WINDOW = 160;
22
20
 
23
- const MAX_CELLS = 60_000;
21
+ const MAX_CELLS = 200_000;
24
22
 
25
- const STUB = 24;
23
+ const STUB = 40;
24
+
25
+ function stubLength(
26
+ from: FlowPoint,
27
+ dir: 1 | -1,
28
+ skip: Rect[],
29
+ clear: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
30
+ ): number {
31
+ let max = STUB;
32
+ const end = from.x + dir * STUB;
33
+ const x0 = Math.min(from.x, end) - 1;
34
+ const x1 = Math.max(from.x, end) + 1;
35
+ for (const rect of clear(x0, from.y - 1, x1, from.y + 1)) {
36
+ if (skip.includes(rect)) continue;
37
+ if (from.y < rect.y - PAD || from.y > rect.y + rect.h + PAD) continue;
38
+ const edge = dir === 1 ? rect.x - PAD : rect.x + rect.w + PAD;
39
+ const room = (edge - from.x) * dir;
40
+ if (room < max) max = Math.max(0, room);
41
+ }
42
+ return max;
43
+ }
44
+
45
+ function growWindow(
46
+ bounds: { x0: number; y0: number; x1: number; y1: number },
47
+ query: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
48
+ pad: number,
49
+ ): void {
50
+ for (let i = 0; i < 4; i += 1) {
51
+ let grew = false;
52
+ for (const rect of query(bounds.x0, bounds.y0, bounds.x1, bounds.y1)) {
53
+ const rx0 = rect.x - pad;
54
+ const ry0 = rect.y - pad;
55
+ const rx1 = rect.x + rect.w + pad;
56
+ const ry1 = rect.y + rect.h + pad;
57
+ if (rx0 < bounds.x0) { bounds.x0 = rx0; grew = true; }
58
+ if (ry0 < bounds.y0) { bounds.y0 = ry0; grew = true; }
59
+ if (rx1 > bounds.x1) { bounds.x1 = rx1; grew = true; }
60
+ if (ry1 > bounds.y1) { bounds.y1 = ry1; grew = true; }
61
+ }
62
+ if (!grew) break;
63
+ }
64
+ }
65
+
66
+ function detourRoute(
67
+ a: FlowPoint,
68
+ b: FlowPoint,
69
+ sa: FlowPoint,
70
+ sb: FlowPoint,
71
+ skip: Rect[],
72
+ query: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
73
+ ): FlowPoint[] {
74
+ const bounds = {
75
+ x0: Math.min(sa.x, sb.x),
76
+ y0: Math.min(sa.y, sb.y),
77
+ x1: Math.max(sa.x, sb.x),
78
+ y1: Math.max(sa.y, sb.y),
79
+ };
80
+ growWindow(bounds, query, PAD);
81
+ const laneAt = (off: number): FlowPoint[][] => [
82
+ [sa, { x: bounds.x1 + off, y: sa.y }, { x: bounds.x1 + off, y: sb.y }, sb],
83
+ [sa, { x: bounds.x0 - off, y: sa.y }, { x: bounds.x0 - off, y: sb.y }, sb],
84
+ [sa, { x: sa.x, y: bounds.y0 - off }, { x: sb.x, y: bounds.y0 - off }, sb],
85
+ [sa, { x: sa.x, y: bounds.y1 + off }, { x: sb.x, y: bounds.y1 + off }, sb],
86
+ ];
87
+ const clearLane = (lane: FlowPoint[]): number | null => {
88
+ let len = 0;
89
+ for (let i = 0; i + 1 < lane.length; i += 1) {
90
+ const p = lane[i]!;
91
+ const q = lane[i + 1]!;
92
+ len += Math.abs(q.x - p.x) + Math.abs(q.y - p.y);
93
+ const sx0 = Math.min(p.x, q.x) - 1;
94
+ const sy0 = Math.min(p.y, q.y) - 1;
95
+ const sx1 = Math.max(p.x, q.x) + 1;
96
+ const sy1 = Math.max(p.y, q.y) + 1;
97
+ for (const rect of query(sx0, sy0, sx1, sy1)) {
98
+ if (skip.includes(rect)) continue;
99
+ return null;
100
+ }
101
+ }
102
+ return len;
103
+ };
104
+ for (let ring = 0; ring < 6; ring += 1) {
105
+ const off = CELL * (ring === 0 ? 1 : 2 ** (ring + 1));
106
+ let best: FlowPoint[] | null = null;
107
+ let bestLen = Infinity;
108
+ for (const lane of laneAt(off)) {
109
+ const len = clearLane(lane);
110
+ if (len !== null && len < bestLen) {
111
+ bestLen = len;
112
+ best = lane;
113
+ }
114
+ }
115
+ if (best) return best;
116
+ growWindow(bounds, query, PAD);
117
+ }
118
+ return zRoute(a, b, sa, sb).slice(1, -1);
119
+ }
26
120
 
27
121
  type Committed = { keys: number[]; x0: number; y0: number; x1: number; y1: number };
28
122
 
29
- type EdgeState = { points: FlowPoint[]; committed?: Committed };
123
+ type RouteEdge = Pick<Edge, 'id' | 'source' | 'target'> & {
124
+ sourceHandle?: string | null;
125
+ targetHandle?: string | null;
126
+ };
127
+
128
+ type EdgeState = {
129
+ points: FlowPoint[];
130
+ a: FlowPoint;
131
+ b: FlowPoint;
132
+ committed?: Committed;
133
+ };
134
+
135
+ const samePoint = (a: FlowPoint, b: FlowPoint) => a.x === b.x && a.y === b.y;
136
+
137
+ const targetInput = (edge: RouteEdge) => {
138
+ const handle = edge.targetHandle;
139
+ const n = handle?.startsWith('in-') ? Number(handle.slice(3)) : 0;
140
+ return Number.isFinite(n) ? n : 0;
141
+ };
142
+
143
+ const sourceOut = (edge: RouteEdge) => {
144
+ const n = /^out-(\d+)$/.exec(edge.sourceHandle ?? '')?.[1];
145
+ return n != null && Number.isFinite(Number(n)) ? Number(n) : 0;
146
+ };
30
147
 
31
148
  export function createCircuitRouter(): (
32
149
  nodes: RouteNode[],
33
- edges: Pick<Edge, 'id' | 'source' | 'target'>[],
34
- direction: CalcDirection,
150
+ edges: RouteEdge[],
35
151
  ) => Map<string, FlowPoint[]> {
36
- let prevDir: CalcDirection | null = null;
37
- let prevRects = new Map<string, Rect>();
38
152
  let prevEdges = new Map<string, EdgeState>();
39
153
 
40
- return (nodes, edges, direction) => {
154
+ return (nodes, edges) => {
41
155
  const out = new Map<string, FlowPoint[]>();
42
156
  const nextEdges = new Map<string, EdgeState>();
43
157
  if (!edges.length) {
44
- prevDir = direction;
45
158
  prevEdges = nextEdges;
46
- prevRects = new Map(nodes.map((n) => [n.id, toRect(n)]));
47
159
  return out;
48
160
  }
49
161
 
@@ -51,11 +163,10 @@ export function createCircuitRouter(): (
51
163
  const rects = new Map(nodes.map((node) => [node.id, toRect(node)]));
52
164
  const rectsInWindow = createRectBuckets(rects.values());
53
165
 
54
- const rebuild = prevDir !== direction;
55
166
  const committed: Committed[] = [];
56
167
  const used = new Set<number>();
57
168
  const pending: {
58
- edge: Pick<Edge, 'id' | 'source' | 'target'>;
169
+ edge: RouteEdge;
59
170
  s: RouteNode;
60
171
  t: RouteNode;
61
172
  a: FlowPoint;
@@ -66,20 +177,11 @@ export function createCircuitRouter(): (
66
177
  for (const edge of edges) {
67
178
  const s = byId.get(edge.source);
68
179
  const t = byId.get(edge.target);
69
- const sRect = rects.get(edge.source);
70
- const tRect = rects.get(edge.target);
180
+ if (!s || !t) continue;
181
+ const a = portAnchor(s, 'src', sourceOut(edge));
182
+ const b = portAnchor(t, 'tgt', targetInput(edge));
71
183
  const prev = prevEdges.get(edge.id);
72
- if (
73
- !rebuild &&
74
- prev &&
75
- sRect &&
76
- tRect &&
77
- prevRects.get(edge.source) &&
78
- prevRects.get(edge.target) &&
79
- sameRect(prevRects.get(edge.source)!, sRect) &&
80
- sameRect(prevRects.get(edge.target)!, tRect)
81
- ) {
82
-
184
+ if (prev && samePoint(prev.a, a) && samePoint(prev.b, b)) {
83
185
  out.set(edge.id, prev.points);
84
186
  nextEdges.set(edge.id, prev);
85
187
  if (prev.committed) {
@@ -88,9 +190,6 @@ export function createCircuitRouter(): (
88
190
  }
89
191
  continue;
90
192
  }
91
- if (!s || !t || !sRect || !tRect) continue;
92
- const a = portAnchor(s, 'src', direction);
93
- const b = portAnchor(t, 'tgt', direction);
94
193
  pending.push({
95
194
  edge,
96
195
  s,
@@ -109,28 +208,34 @@ export function createCircuitRouter(): (
109
208
 
110
209
  for (const job of pending) {
111
210
  const { a, b } = job;
112
- const horizontal = direction === 'horizontal';
113
-
114
- const gap = horizontal ? a.x - b.x : a.y - b.y;
115
- if (gap > 0 && (horizontal ? a.y === b.y : a.x === b.x)) {
116
211
 
212
+ const gap = b.x - a.x;
213
+ if (gap > 0 && a.y === b.y) {
117
214
  const points = [a, b];
118
215
  out.set(job.edge.id, points);
119
- nextEdges.set(job.edge.id, { points });
216
+ nextEdges.set(job.edge.id, { points, a, b });
120
217
  continue;
121
218
  }
122
219
 
123
- const stub = gap > 0 ? Math.min(STUB, gap / 2) : STUB;
124
- const sa = horizontal
125
- ? { x: a.x - stub, y: a.y }
126
- : { x: a.x, y: a.y - stub };
127
- const sb = horizontal
128
- ? { x: b.x + stub, y: b.y }
129
- : { x: b.x, y: b.y + stub };
130
- const wx0 = Math.min(sa.x, sb.x) - WINDOW;
131
- const wy0 = Math.min(sa.y, sb.y) - WINDOW;
132
- const wx1 = Math.max(sa.x, sb.x) + WINDOW;
133
- const wy1 = Math.max(sa.y, sb.y) + WINDOW;
220
+ const sa = {
221
+ x: a.x + stubLength(a, 1, [rects.get(job.s.id)!], rectsInWindow),
222
+ y: a.y,
223
+ };
224
+ const sb = {
225
+ x: b.x - stubLength(b, -1, [rects.get(job.t.id)!], rectsInWindow),
226
+ y: b.y,
227
+ };
228
+ const bounds = {
229
+ x0: Math.min(sa.x, sb.x) - WINDOW,
230
+ y0: Math.min(sa.y, sb.y) - WINDOW,
231
+ x1: Math.max(sa.x, sb.x) + WINDOW,
232
+ y1: Math.max(sa.y, sb.y) + WINDOW,
233
+ };
234
+ growWindow(bounds, rectsInWindow, PAD + CELL);
235
+ const wx0 = bounds.x0;
236
+ const wy0 = bounds.y0;
237
+ const wx1 = bounds.x1;
238
+ const wy1 = bounds.y1;
134
239
  const ox = Math.floor(wx0 / CELL);
135
240
  const oy = Math.floor(wy0 / CELL);
136
241
  const gw = Math.ceil(wx1 / CELL) - ox + 1;
@@ -165,10 +270,11 @@ export function createCircuitRouter(): (
165
270
  x: Math.min(gw - 1, Math.max(0, Math.round(sb.x / CELL) - ox)),
166
271
  y: Math.min(gh - 1, Math.max(0, Math.round(sb.y / CELL) - oy)),
167
272
  };
168
- for (let pass = 0; pass < 2 && !cells; pass += 1) {
273
+ let best: number[][] | null = null;
274
+ let bestGrid: InstanceType<typeof PF.Grid> | null = null;
275
+ for (let pass = 0; pass < 2; pass += 1) {
169
276
  const grid = base.clone();
170
277
  if (pass === 0) {
171
-
172
278
  for (const c of committed) {
173
279
  if (
174
280
  c.x1 < ox ||
@@ -196,17 +302,36 @@ export function createCircuitRouter(): (
196
302
  tCell.y,
197
303
  grid,
198
304
  );
199
- if (path.length) {
200
- cells = path.map(([cx, cy]) => [cx + ox, cy + oy]);
201
- passGrid = grid;
305
+ if (!path.length) continue;
306
+ const mapped = path.map(([cx, cy]) => [cx + ox, cy + oy]);
307
+ if (pass === 1) {
308
+ if (best && best.length <= mapped.length * 1.34) break;
309
+ best = mapped;
310
+ bestGrid = grid;
311
+ break;
202
312
  }
313
+ best = mapped;
314
+ bestGrid = grid;
203
315
  }
316
+ cells = best;
317
+ passGrid = bestGrid;
204
318
  }
205
319
 
206
320
  if (!cells || !passGrid) {
207
- const points = orthoPoints(zRoute(a, b, sa, sb, direction), direction);
321
+ const points = orthoPoints([
322
+ a,
323
+ ...detourRoute(
324
+ a,
325
+ b,
326
+ sa,
327
+ sb,
328
+ [rects.get(job.s.id)!, rects.get(job.t.id)!],
329
+ rectsInWindow,
330
+ ),
331
+ b,
332
+ ]);
208
333
  out.set(job.edge.id, points);
209
- nextEdges.set(job.edge.id, { points });
334
+ nextEdges.set(job.edge.id, { points, a, b });
210
335
  continue;
211
336
  }
212
337
 
@@ -221,7 +346,7 @@ export function createCircuitRouter(): (
221
346
  g.isWalkableAt(x - ox, y - oy),
222
347
  );
223
348
  const polyline = cellsToPolyline(simple, sa, sb);
224
- const points = orthoPoints([a, sa, ...polyline, sb, b], direction);
349
+ const points = orthoPoints([a, sa, ...polyline, sb, b]);
225
350
  out.set(job.edge.id, points);
226
351
 
227
352
  const keys: number[] = [];
@@ -259,12 +384,11 @@ export function createCircuitRouter(): (
259
384
  y1: cy1 === -Infinity ? 0 : cy1,
260
385
  };
261
386
  committed.push(entry);
262
- nextEdges.set(job.edge.id, { points, committed: entry });
387
+ nextEdges.set(job.edge.id, { points, a: job.a, b: job.b, committed: entry });
263
388
  }
264
389
 
265
- prevDir = direction;
266
- prevRects = rects;
267
390
  prevEdges = nextEdges;
268
391
  return out;
269
392
  };
270
393
  }
394
+