@terrafirmagreg-team/recipe 0.1.0 → 1.2.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 (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,7 +1,13 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
1
2
  import type { Edge, Node } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
2
3
  import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
3
- import type { CalcKind, CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
4
- import type { CalcGoal } from '@terrafirmagreg-team/recipe/lib/calc/tree';
4
+ import { PORT_IN } from '@terrafirmagreg-team/recipe/lib/calc/ports';
5
+ import type {
6
+ CalcKind,
7
+ CalcNode,
8
+ CalcRecipeSummary,
9
+ CalcWire,
10
+ } from '@terrafirmagreg-team/recipe/types/calc';
5
11
  import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
6
12
 
7
13
  export const CALC_NODE_W = 232;
@@ -14,56 +20,61 @@ const CALC_MINI_W = 96;
14
20
  const CALC_MINI_TAG_H = 84;
15
21
 
16
22
  export type CalcViewMode = 'mini' | 'normal' | 'detailed';
17
- export type CalcDirection = 'vertical' | 'horizontal';
18
- export type CalcEdgeStyle =
19
- | 'default'
20
- | 'straight'
21
- | 'step'
22
- | 'smoothstep'
23
- | 'circuit';
24
23
  export type CalcFarMode = 'full' | 'preview' | 'hidden';
25
24
 
26
- export type CalcDensity = number;
27
-
28
- const DENSITY_GAP_MIN = { x: 8, y: 12, goal: 28 };
29
- const DENSITY_GAP_MAX = { x: 14, y: 22, goal: 44 };
30
-
31
- export function densityGaps(density: CalcDensity) {
32
- const d = Math.min(1, Math.max(0, density || 0));
33
- return {
34
- x: DENSITY_GAP_MIN.x + (DENSITY_GAP_MAX.x - DENSITY_GAP_MIN.x) * d,
35
- y: DENSITY_GAP_MIN.y + (DENSITY_GAP_MAX.y - DENSITY_GAP_MIN.y) * d,
36
- goal:
37
- DENSITY_GAP_MIN.goal +
38
- (DENSITY_GAP_MAX.goal - DENSITY_GAP_MIN.goal) * d,
39
- };
40
- }
25
+ export const GRID_GAPS = { x: 14, y: 24, goal: 48 };
41
26
 
42
27
  export type CalcTier = 'dot' | 'far' | 'near';
43
28
 
44
29
  export type CalcFlowData = {
45
30
  node: CalcNode;
46
- dataset: string;
31
+ dataset: DatasetRef;
47
32
  mode: CalcViewMode;
48
- direction: CalcDirection;
49
-
50
- missing?: CalcNode[];
51
33
  recipeBody: (recipeId: string) => Recipe | undefined;
52
- onSelectRecipe: (materialId: string, recipeId: string | null) => void;
34
+ onSelectRecipe: (cardId: string, recipeId: string | null) => void;
53
35
  onSelectTagMember: (tagId: string, memberId: string) => void;
54
-
55
36
  onRequestPicker: (node: CalcNode) => void;
56
-
57
37
  onRequestMemberPicker: (node: CalcNode) => void;
58
-
59
- onSetTarget: (materialId: string, kind: CalcKind) => void;
60
-
61
- onAddStock: (materialId: string, kind: CalcKind) => void;
38
+ onRequestSupply: (node: CalcNode, input: number) => void;
39
+ onSupplyInput: (
40
+ cardId: string,
41
+ inputIndex: number,
42
+ recipeId: string | null,
43
+ outputAmount?: number,
44
+ ) => string | null;
45
+ getRecipesForItem: (
46
+ materialId: string,
47
+ kind: CalcKind,
48
+ ) => CalcRecipeSummary[];
49
+ ensureMaterial: (materialId: string) => void;
50
+ onAddCard: (
51
+ materialId: string,
52
+ kind: CalcKind,
53
+ pos?: { x: number; y: number },
54
+ ) => string;
62
55
  onOpenItem: (materialId: string, kind: CalcKind) => void;
63
- onFillNode: (path: string) => void;
64
- onCollapseNode: (path: string) => void;
65
-
66
- onRemoveTarget: (flowId: string) => void;
56
+ onRemoveCard: (cardId: string) => void;
57
+ onMoveCard: (cardId: string, pos: { x: number; y: number }) => void;
58
+ onCrafts: (cardId: string, crafts: number) => void;
59
+ onWire: (from: string, to: string, input: number, out?: number) => void;
60
+ onUnwire: (
61
+ from: string,
62
+ to: string,
63
+ input: number,
64
+ out?: number,
65
+ ) => void;
66
+ onRewire: (
67
+ from: string,
68
+ to: string,
69
+ input: number,
70
+ next: { from: string; to: string; input: number; out?: number },
71
+ ) => void;
72
+ onWireAmount: (
73
+ from: string,
74
+ to: string,
75
+ input: number,
76
+ amount: number | null,
77
+ ) => void;
67
78
  recipeDefaults: Record<string, string>;
68
79
  onSetRecipeDefault: (materialId: string, recipeId: string | null) => void;
69
80
  };
@@ -75,86 +86,115 @@ export function nodeSize(
75
86
  calc: CalcNode,
76
87
  ctx: { mode: CalcViewMode; recipeBody: (id: string) => Recipe | undefined },
77
88
  ): { w: number; h: number } {
89
+ const margin =
90
+ (calc.inputs.length ? PORT_IN : 0) + (calc.outputs.length ? PORT_IN : 0);
78
91
  if (ctx.mode === 'mini') {
79
-
80
- return { w: CALC_MINI_W, h: CALC_MINI_TAG_H };
92
+ return { w: CALC_MINI_W + margin, h: CALC_MINI_TAG_H };
81
93
  }
82
94
  const body = calc.recipe ? ctx.recipeBody(calc.recipe.recipeId) : undefined;
83
95
  if (ctx.mode === 'detailed' && body?.w && body.h) {
84
96
  return {
85
- w: Math.max(CALC_NODE_W, body.w + 16),
97
+ w: Math.max(CALC_NODE_W, body.w + 16) + margin,
86
98
  h: CALC_NODE_H + body.h + 6,
87
99
  };
88
100
  }
89
- return { w: CALC_NODE_W, h: CALC_NODE_H };
101
+ return { w: CALC_NODE_W + margin, h: CALC_NODE_H };
90
102
  }
91
103
 
92
- export function goalsToFlow(
93
- goals: CalcGoal[],
104
+ export function cardsToFlow(
105
+ nodes: CalcNode[],
106
+ wires: readonly CalcWire[],
94
107
  data: Omit<CalcFlowData, 'node'>,
95
- edgeStyle: CalcEdgeStyle = 'default',
108
+ flows?: readonly (number | undefined)[],
96
109
  ): { nodes: AnyCalcFlowNode[]; edges: Edge[] } {
97
- const nodes: AnyCalcFlowNode[] = [];
98
- const edges: Edge[] = [];
99
- const walk = (node: CalcNode, path: string): void => {
110
+ const flowNodes: AnyCalcFlowNode[] = nodes.map((node) => {
100
111
  const { w, h } = nodeSize(node, {
101
112
  mode: data.mode,
102
113
  recipeBody: data.recipeBody,
103
114
  });
104
- const missing = node.children.filter((child) => child.collapsed);
105
- nodes.push({
106
- id: path,
115
+ return {
116
+ id: node.id,
107
117
  type: 'calcMaterial',
108
118
  position: { x: 0, y: 0 },
109
119
  width: w,
110
120
  height: h,
111
- data: { ...data, node, ...(missing.length ? { missing } : {}) },
112
- });
113
- node.children.forEach((child, index) => {
114
- if (child.collapsed) return;
115
- const childPath = child.path ?? `${path}/${index}`;
116
- edges.push({
117
- id: `${childPath}->${path}`,
118
- source: childPath,
119
- target: path,
121
+ data: { ...data, node },
122
+ };
123
+ });
124
+ const byId = new Map(nodes.map((node) => [node.id, node]));
125
+ const edges: Edge[] = wires.flatMap((wire, index) => {
126
+ const src = byId.get(wire.from);
127
+ const dst = byId.get(wire.to);
128
+ const input = dst?.inputs[wire.input];
129
+ if (!src || !dst || !input || !input.wired) return [];
130
+ const atIndex = src.outputs[wire.out ?? 0];
131
+ const outIndex =
132
+ atIndex && atIndex.id === input.id && atIndex.kind === input.kind
133
+ ? (wire.out ?? 0)
134
+ : src.outputs.findIndex(
135
+ (o) => o.id === input.id && o.kind === input.kind,
136
+ );
137
+ if (outIndex < 0) return [];
138
+ const mat = src.outputs[outIndex];
139
+ return [
140
+ {
141
+ id: `wire:${wire.from}->${wire.to}:${wire.input}:${index}`,
142
+ source: wire.from,
143
+ sourceHandle: outIndex > 0 ? `out-${outIndex}` : 'out',
144
+ target: wire.to,
145
+ targetHandle: `in-${wire.input}`,
120
146
  type: 'calcEdge',
121
- className:
122
- [
123
- child.catalyst ? 'calc-edge--catalyst' : '',
124
- edgeStyle === 'circuit' ? 'calc-edge--circuit' : '',
125
- ]
126
- .filter(Boolean)
127
- .join(' ') || undefined,
128
- data: { route: edgeStyle },
129
- });
130
- walk(child, childPath);
131
- });
132
- };
133
- goals.forEach((goal, gi) => walk(goal.node, `g${gi}`));
134
- return { nodes, edges };
147
+ className: 'calc-edge--wire',
148
+ deletable: true,
149
+ data: {
150
+ wire,
151
+ flow: flows?.[index],
152
+ matId: mat?.id ?? input.id,
153
+ matKind: mat?.kind ?? input.kind,
154
+ dataset: data.dataset,
155
+ onWireAmount: data.onWireAmount,
156
+ onUnwire: data.onUnwire,
157
+ },
158
+ },
159
+ ];
160
+ });
161
+ return { nodes: flowNodes, edges };
135
162
  }
136
163
 
137
164
  function sameCalcNode(a: CalcNode, b: CalcNode): boolean {
138
165
  return (
166
+ a.id === b.id &&
139
167
  a.materialId === b.materialId &&
140
168
  a.kind === b.kind &&
141
- a.amount === b.amount &&
142
- a.batches === b.batches &&
143
- a.satisfied === b.satisfied &&
144
- a.need === b.need &&
169
+ a.crafts === b.crafts &&
145
170
  a.produced === b.produced &&
171
+ a.allocated === b.allocated &&
172
+ a.remaining === b.remaining &&
173
+ a.outputs.length === b.outputs.length &&
174
+ a.outputs.every(
175
+ (out, index) =>
176
+ out.id === b.outputs[index]?.id &&
177
+ out.kind === b.outputs[index]?.kind &&
178
+ out.amount === b.outputs[index]?.amount &&
179
+ out.allocated === b.outputs[index]?.allocated &&
180
+ out.remaining === b.outputs[index]?.remaining,
181
+ ) &&
146
182
  a.cycle === b.cycle &&
147
- a.catalyst === b.catalyst &&
148
- a.collapsed === b.collapsed &&
183
+ a.products === b.products &&
149
184
  a.recipe?.recipeId === b.recipe?.recipeId &&
150
185
  a.recipe?.category === b.recipe?.category &&
151
186
  a.tagId === b.tagId &&
152
- a.path === b.path &&
153
- a.expandable === b.expandable &&
154
- a.auto === b.auto &&
155
187
  a.loading === b.loading &&
188
+ a.inputs.length === b.inputs.length &&
189
+ a.inputs.every(
190
+ (inp, index) =>
191
+ inp.id === b.inputs[index]?.id &&
192
+ inp.amount === b.inputs[index]?.amount &&
193
+ inp.covered === b.inputs[index]?.covered &&
194
+ inp.wired === b.inputs[index]?.wired &&
195
+ inp.catalyst === b.inputs[index]?.catalyst,
196
+ ) &&
156
197
  (a.members?.join() ?? '') === (b.members?.join() ?? '') &&
157
- a.children.length === b.children.length &&
158
198
  a.availableRecipes.length === b.availableRecipes.length &&
159
199
  a.availableRecipes.every(
160
200
  (row, index) =>
@@ -165,38 +205,29 @@ function sameCalcNode(a: CalcNode, b: CalcNode): boolean {
165
205
  );
166
206
  }
167
207
 
168
- function sameMissing(
169
- a: CalcNode[] | undefined,
170
- b: CalcNode[] | undefined,
171
- ): boolean {
172
- if ((a?.length ?? 0) !== (b?.length ?? 0)) return false;
173
- return (a ?? []).every(
174
- (node, i) =>
175
- node.path === b![i]!.path &&
176
- node.materialId === b![i]!.materialId &&
177
- node.need === b![i]!.need &&
178
- node.availableRecipes.length === b![i]!.availableRecipes.length,
179
- );
180
- }
181
-
182
208
  function sameFlowData(a: CalcFlowData, b: CalcFlowData): boolean {
183
209
  return (
184
210
  (a.node === b.node || sameCalcNode(a.node, b.node)) &&
185
211
  a.dataset === b.dataset &&
186
212
  a.mode === b.mode &&
187
- a.direction === b.direction &&
188
- sameMissing(a.missing, b.missing) &&
189
- a.onFillNode === b.onFillNode &&
190
- a.onCollapseNode === b.onCollapseNode &&
191
- a.onRemoveTarget === b.onRemoveTarget &&
192
213
  a.recipeBody === b.recipeBody &&
193
214
  a.onSelectRecipe === b.onSelectRecipe &&
194
215
  a.onSelectTagMember === b.onSelectTagMember &&
195
216
  a.onRequestPicker === b.onRequestPicker &&
196
217
  a.onRequestMemberPicker === b.onRequestMemberPicker &&
197
- a.onSetTarget === b.onSetTarget &&
198
- a.onAddStock === b.onAddStock &&
218
+ a.onRequestSupply === b.onRequestSupply &&
219
+ a.onSupplyInput === b.onSupplyInput &&
220
+ a.getRecipesForItem === b.getRecipesForItem &&
221
+ a.ensureMaterial === b.ensureMaterial &&
222
+ a.onAddCard === b.onAddCard &&
199
223
  a.onOpenItem === b.onOpenItem &&
224
+ a.onRemoveCard === b.onRemoveCard &&
225
+ a.onMoveCard === b.onMoveCard &&
226
+ a.onCrafts === b.onCrafts &&
227
+ a.onWire === b.onWire &&
228
+ a.onUnwire === b.onUnwire &&
229
+ a.onRewire === b.onRewire &&
230
+ a.onWireAmount === b.onWireAmount &&
200
231
  a.recipeDefaults === b.recipeDefaults &&
201
232
  a.onSetRecipeDefault === b.onSetRecipeDefault
202
233
  );
@@ -258,8 +289,12 @@ export function reconcileCalcEdges(
258
289
  kept &&
259
290
  kept.source === edge.source &&
260
291
  kept.target === edge.target &&
292
+ kept.sourceHandle === edge.sourceHandle &&
293
+ kept.targetHandle === edge.targetHandle &&
261
294
  kept.className === edge.className &&
262
- (kept.data?.route ?? 'default') === (edge.data?.route ?? 'default') &&
295
+ kept.data?.flow === edge.data?.flow &&
296
+ (kept.data?.wire as CalcWire | undefined)?.amount ===
297
+ (edge.data?.wire as CalcWire | undefined)?.amount &&
263
298
  ((kept.data?.points as FlowPoint[] | undefined)?.length ?? 0) ===
264
299
  ((edge.data?.points as FlowPoint[] | undefined)?.length ?? 0) &&
265
300
  (kept.data?.points as FlowPoint[] | undefined)?.every(
@@ -277,3 +312,4 @@ export function reconcileCalcEdges(
277
312
  });
278
313
  return changed ? out : prev;
279
314
  }
315
+
@@ -12,3 +12,4 @@ export function onCalcFocusRequest(listener: Listener): () => void {
12
12
  listeners.delete(listener);
13
13
  };
14
14
  }
15
+
@@ -1,9 +1,10 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
1
2
  import type { CalcKind } from '@terrafirmagreg-team/recipe/types/calc';
2
3
 
3
4
  export type IconItem = { id: string; kind: CalcKind };
4
5
 
5
6
  export type EntryMeta = {
6
- dataset: string;
7
+ dataset: DatasetRef;
7
8
  lang: string;
8
9
  langMap?: Record<string, string>;
9
10
  iconItems?: IconItem[];
@@ -13,3 +14,4 @@ export const rowClass =
13
14
  'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors';
14
15
  export const rowIdle =
15
16
  'text-muted-foreground hover:bg-accent hover:text-accent-foreground';
17
+
@@ -1,184 +1,77 @@
1
- import potpack, { type PotpackBox } from 'potpack';
2
1
  import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
3
- import type { CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
4
- import type { CalcGoal } from '@terrafirmagreg-team/recipe/lib/calc/tree';
2
+ import type {
3
+ CalcNode,
4
+ CalcWire,
5
+ } from '@terrafirmagreg-team/recipe/types/calc';
5
6
  import {
6
7
  nodeSize,
7
- type CalcDirection,
8
8
  type CalcViewMode,
9
9
  } from '@terrafirmagreg-team/recipe/lib/calc/flow';
10
10
 
11
11
  export type CalcPackOptions = {
12
12
  mode: CalcViewMode;
13
- direction: CalcDirection;
14
13
  gaps: { x: number; y: number; goal: number };
15
14
  recipeBody: (recipeId: string) => Recipe | undefined;
16
15
  };
17
16
 
18
- type Packed = {
19
- node: CalcNode;
20
- path: string;
21
-
22
- fragW: number;
23
-
24
- fragH: number;
25
-
26
- kidRows: Packed[][];
27
-
28
- wrapped: Packed[];
29
- };
30
-
31
- export function packGoals(
32
- goals: CalcGoal[],
17
+ export function layoutCards(
18
+ nodes: CalcNode[],
19
+ wires: readonly CalcWire[],
33
20
  options: CalcPackOptions,
34
21
  ): Map<string, { x: number; y: number }> {
35
- const vertical = options.direction !== 'horizontal';
36
22
  const { gaps } = options;
37
23
  const sizeOf = (node: CalcNode) =>
38
24
  nodeSize(node, {
39
25
  mode: options.mode,
40
26
  recipeBody: options.recipeBody,
41
27
  });
42
- const cross = (node: CalcNode) =>
43
- vertical ? sizeOf(node).w : sizeOf(node).h;
44
- const main = (node: CalcNode) =>
45
- vertical ? sizeOf(node).h : sizeOf(node).w;
46
-
47
- const measure = (
48
- node: CalcNode,
49
- path: string,
50
- budget: number,
51
- rowCap: number,
52
- ): Packed => {
53
- const kids = node.children
54
- .filter((child) => !child.collapsed)
55
- .map((child, index) =>
56
- measure(child, child.path ?? `${path}/${index}`, budget, rowCap),
57
- );
58
- const bands: Packed[][] = [];
59
- let row: Packed[] = [];
60
- let rowW = 0;
61
- for (const kid of kids) {
62
- if (row.length && rowW + gaps.x + kid.fragW > rowCap) {
63
- bands.push(row);
64
- row = [];
65
- rowW = 0;
66
- }
67
- rowW += (row.length ? gaps.x : 0) + kid.fragW;
68
- row.push(kid);
69
- }
70
- if (row.length) bands.push(row);
71
-
72
- const kidRows: Packed[][] = [];
73
- const wrapped: Packed[] = [];
74
- let used = main(node);
75
- for (const band of bands) {
76
- const bandH = Math.max(...band.map((kid) => kid.fragH));
77
- if (kidRows.length && used + gaps.y + bandH > budget) {
78
- wrapped.push(...band);
79
- continue;
80
- }
81
- kidRows.push(band);
82
- used += gaps.y + bandH;
83
- }
84
- const widest = kidRows.reduce(
85
- (acc, band) =>
86
- Math.max(
87
- acc,
88
- band.reduce((sum, kid) => sum + kid.fragW, 0) +
89
- gaps.x * (band.length - 1),
90
- ),
91
- 0,
92
- );
93
- return {
94
- node,
95
- path,
96
- fragW: Math.max(cross(node), widest),
97
- fragH: used,
98
- kidRows,
99
- wrapped,
100
- };
101
- };
28
+ const byId = new Map(nodes.map((node) => [node.id, node]));
29
+ const consumers = new Map<string, string[]>();
30
+ const suppliers = new Map<string, string[]>();
31
+ for (const wire of wires) {
32
+ if (!byId.has(wire.from) || !byId.has(wire.to)) continue;
33
+ consumers.set(wire.from, [...(consumers.get(wire.from) ?? []), wire.to]);
34
+ suppliers.set(wire.to, [...(suppliers.get(wire.to) ?? []), wire.from]);
35
+ }
102
36
 
103
- const collect = (tree: Packed, out: Packed[]) => {
104
- for (const kid of tree.wrapped) {
105
- out.push(kid);
106
- collect(kid, out);
107
- }
108
- for (const row of tree.kidRows) {
109
- for (const kid of row) collect(kid, out);
37
+ const depth = new Map<string, number>();
38
+ const visit = (id: string, trail: Set<string>): number => {
39
+ const hit = depth.get(id);
40
+ if (hit !== undefined) return hit;
41
+ if (trail.has(id)) return 0;
42
+ trail.add(id);
43
+ let d = 0;
44
+ for (const next of suppliers.get(id) ?? []) {
45
+ d = Math.max(d, visit(next, trail) + 1);
110
46
  }
47
+ trail.delete(id);
48
+ depth.set(id, d);
49
+ return d;
111
50
  };
112
- const measureAll = (budget: number, rowCap: number) =>
113
- goals.map((goal, gi) => {
114
- const root = measure(goal.node, `g${gi}`, budget, rowCap);
115
- const columns = [root];
116
- collect(root, columns);
117
- return columns;
118
- });
51
+ for (const node of nodes) visit(node.id, new Set());
119
52
 
120
- let totalArea = 0;
121
- let maxMain = 0;
122
- for (const columns of measureAll(Infinity, Infinity)) {
123
- for (const column of columns) {
124
- totalArea +=
125
- (column.fragW + gaps.goal) * (column.fragH + gaps.goal);
126
- maxMain = Math.max(maxMain, main(column.node));
127
- }
53
+ const lanes = new Map<number, CalcNode[]>();
54
+ for (const node of nodes) {
55
+ const d = depth.get(node.id) ?? 0;
56
+ lanes.set(d, [...(lanes.get(d) ?? []), node]);
128
57
  }
129
-
130
- const target = Math.max(Math.sqrt(totalArea), maxMain * 2);
131
- const sheets = measureAll(target, target);
132
-
133
58
  const positions = new Map<string, { x: number; y: number }>();
134
- const place = (
135
- tree: Packed,
136
- left: number,
137
- top: number,
138
- rowH: number,
139
- ) => {
140
- positions.set(tree.path, {
141
- x: vertical ? left + tree.fragW / 2 : top + rowH / 2,
142
- y: vertical ? top + rowH / 2 : left + tree.fragW / 2,
143
- });
144
- let bandTop = top + rowH + gaps.y;
145
- for (const row of tree.kidRows) {
146
- const rowW =
147
- row.reduce((sum, kid) => sum + kid.fragW, 0) +
148
- gaps.x * (row.length - 1);
149
- const cardRowH = Math.max(...row.map((kid) => main(kid.node)));
150
- const bandH = Math.max(...row.map((kid) => kid.fragH));
151
- let cursor = left + (tree.fragW - rowW) / 2;
152
- for (const kid of row) {
153
- place(kid, cursor, bandTop, cardRowH);
154
- cursor += kid.fragW + gaps.x;
155
- }
156
- bandTop += bandH + gaps.y;
157
- }
158
- };
159
-
160
- type FragBox = PotpackBox & { frag: Packed };
161
- const packedSheets: (PotpackBox & { boxes: FragBox[] })[] = sheets.map(
162
- (columns) => {
163
- const boxes: FragBox[] = columns.map((frag) => ({
164
- w: frag.fragW + gaps.goal,
165
- h: frag.fragH + gaps.goal,
166
- frag,
167
- }));
168
- const sheet = potpack(boxes);
169
- return { w: sheet.w + gaps.goal, h: sheet.h + gaps.goal, boxes };
170
- },
171
- );
172
- potpack(packedSheets);
173
- for (const sheet of packedSheets) {
174
- for (const box of sheet.boxes) {
175
- place(
176
- box.frag,
177
- (sheet.x ?? 0) + (box.x ?? 0),
178
- (sheet.y ?? 0) + (box.y ?? 0),
179
- main(box.frag.node),
180
- );
59
+ let mainCursor = 0;
60
+ const ordered = [...lanes.entries()].sort(([a], [b]) => a - b);
61
+ for (const [, lane] of ordered) {
62
+ let cross = 0;
63
+ let laneMain = 0;
64
+ for (const node of lane) {
65
+ const { w, h } = sizeOf(node);
66
+ positions.set(node.id, {
67
+ x: mainCursor + w / 2,
68
+ y: cross + h / 2,
69
+ });
70
+ cross += h + gaps.x;
71
+ laneMain = Math.max(laneMain, w);
181
72
  }
73
+ mainCursor += laneMain + gaps.y + gaps.goal;
182
74
  }
183
75
  return positions;
184
76
  }
77
+
@@ -0,0 +1,43 @@
1
+ export const PORT_SIZE = 20;
2
+ export const PORT_GAP = 2;
3
+ export const PORT_STEP = PORT_SIZE + PORT_GAP;
4
+ export const PORT_OUT = PORT_SIZE / 2;
5
+ export const PORT_IN = PORT_OUT + 2;
6
+
7
+ export const railSpans = new Map<
8
+ string,
9
+ {
10
+ inStart?: number;
11
+ inRows?: number;
12
+ outStart?: number;
13
+ outRows?: number;
14
+ }
15
+ >();
16
+
17
+ export function portAnchorPoint(
18
+ node: { position: { x: number; y: number }; width?: number; height?: number },
19
+ side: 'src' | 'tgt',
20
+ index = 0,
21
+ count = 1,
22
+ ): { x: number; y: number } {
23
+ const w = node.width ?? 0;
24
+ const h = node.height ?? 0;
25
+ const span =
26
+ 'id' in node && typeof node.id === 'string'
27
+ ? railSpans.get(node.id)
28
+ : undefined;
29
+ const start = (side === 'tgt' ? span?.inStart : span?.outStart) ?? 0;
30
+ const measured = side === 'tgt' ? span?.inRows : span?.outRows;
31
+ const maxRows = Math.max(1, Math.floor((h - 4) / PORT_STEP));
32
+ const rows = Math.max(1, Math.min(count, measured ?? maxRows));
33
+ const row = Math.max(
34
+ 0,
35
+ Math.min(index - start + (start > 0 ? 1 : 0), rows - 1),
36
+ );
37
+ const sign = side === 'src' ? 1 : -1;
38
+ return {
39
+ x: node.position.x + sign * (w / 2),
40
+ y: node.position.y + (row - (rows - 1) / 2) * PORT_STEP,
41
+ };
42
+ }
43
+