@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
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ type Dispatch,
9
+ type RefObject,
10
+ type SetStateAction,
11
+ } from 'react';
12
+ import type { Edge } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
13
+ import type {
14
+ AnyCalcFlowNode,
15
+ cardsToFlow,
16
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
17
+ import type { ChevronEdgeHighlight } from '@terrafirmagreg-team/ui/types/canvas';
18
+ import type { MountedNodes } from './use-mount';
19
+
20
+ const CHAIN_NODE_MARK = 'calc-node--chain';
21
+ const DIM_NODE_MARK = 'calc-node--dim';
22
+
23
+ function stripMark(cls: string | undefined) {
24
+ return cls
25
+ ?.split(' ')
26
+ .filter((c) => c !== CHAIN_NODE_MARK && c !== DIM_NODE_MARK)
27
+ .join(' ');
28
+ }
29
+
30
+ export function useEdgeEngage({
31
+ computed,
32
+ setState,
33
+ rawFlow,
34
+ focusIdRef,
35
+ }: {
36
+ computed: { nodes: AnyCalcFlowNode[]; edges: Edge[] };
37
+ setState: Dispatch<SetStateAction<MountedNodes>>;
38
+ rawFlow: ReturnType<typeof cardsToFlow>;
39
+ focusIdRef: RefObject<string | null>;
40
+ }) {
41
+ const engagedRef = useRef<string | null>(null);
42
+ const adjacency = useMemo(() => {
43
+ const downstream = new Map<string, Edge[]>();
44
+ const upstream = new Map<string, Edge[]>();
45
+ for (const edge of computed.edges) {
46
+ downstream.set(edge.source, [
47
+ ...(downstream.get(edge.source) ?? []),
48
+ edge,
49
+ ]);
50
+ upstream.set(edge.target, [...(upstream.get(edge.target) ?? []), edge]);
51
+ }
52
+ return { downstream, upstream };
53
+ }, [computed]);
54
+
55
+ const applyEngaged = useCallback(
56
+ (id: string | null) => {
57
+ const prev = engagedRef.current;
58
+ if (prev === id) return;
59
+ engagedRef.current = id;
60
+
61
+ const chainEdges = new Map<string, ChevronEdgeHighlight>();
62
+ const chainNodes = new Set<string>();
63
+ if (id) {
64
+ chainNodes.add(id);
65
+ const walk = (
66
+ start: string,
67
+ edgesOf: (n: string) => Edge[] | undefined,
68
+ nextOf: (e: Edge) => string,
69
+ mark: ChevronEdgeHighlight,
70
+ ) => {
71
+ const seen = new Set<string>([start]);
72
+ const queue = [start];
73
+ while (queue.length) {
74
+ const nodeId = queue.shift()!;
75
+ for (const edge of edgesOf(nodeId) ?? []) {
76
+ chainEdges.set(edge.id, mark);
77
+ const next = nextOf(edge);
78
+ if (!seen.has(next)) {
79
+ seen.add(next);
80
+ chainNodes.add(next);
81
+ queue.push(next);
82
+ }
83
+ }
84
+ }
85
+ };
86
+ walk(id, (n) => adjacency.upstream.get(n), (e) => e.source, 'incoming');
87
+ walk(id, (n) => adjacency.downstream.get(n), (e) => e.target, 'outgoing');
88
+ }
89
+ const active = chainEdges.size > 0;
90
+
91
+ setState((current) => {
92
+ let edgesChanged = false;
93
+ const edges = current.edges.map((edge) => {
94
+ const mark = active
95
+ ? (chainEdges.get(edge.id) ?? 'dim')
96
+ : undefined;
97
+ if ((edge.data?.highlight ?? undefined) === mark) return edge;
98
+ edgesChanged = true;
99
+ return { ...edge, data: { ...edge.data, highlight: mark } };
100
+ });
101
+ let nodesChanged = false;
102
+ const nodes = current.nodes.map((node) => {
103
+ const base = stripMark(node.className);
104
+ const cls = active
105
+ ? chainNodes.has(node.id)
106
+ ? `${base ?? ''} ${CHAIN_NODE_MARK}`.trim()
107
+ : `${base ?? ''} ${DIM_NODE_MARK}`.trim()
108
+ : base;
109
+ if ((node.className ?? '') === (cls ?? '')) return node;
110
+ nodesChanged = true;
111
+ return { ...node, className: cls || undefined };
112
+ });
113
+ if (!edgesChanged && !nodesChanged) return current;
114
+ return {
115
+ ...current,
116
+ edges: edgesChanged ? edges : current.edges,
117
+ nodes: nodesChanged ? nodes : current.nodes,
118
+ };
119
+ });
120
+ },
121
+ [adjacency, setState],
122
+ );
123
+ useEffect(() => {
124
+ engagedRef.current = null;
125
+ focusIdRef.current = null;
126
+ }, [rawFlow, focusIdRef]);
127
+ return applyEngaged;
128
+ }
129
+
@@ -0,0 +1,192 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ type DragEvent,
6
+ type MouseEvent,
7
+ } from 'react';
8
+ import {
9
+ useReactFlow,
10
+ type Connection,
11
+ type Edge,
12
+ } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
13
+ import type {
14
+ AnyCalcFlowNode,
15
+ CalcFlowData,
16
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
17
+ import type {
18
+ CalcKind,
19
+ CalcNode,
20
+ } from '@terrafirmagreg-team/recipe/types/calc';
21
+
22
+ export function useFlowHandlers({
23
+ data,
24
+ applyFocus,
25
+ applyEngaged,
26
+ centerOnNode,
27
+ setPickerNode,
28
+ }: {
29
+ data: Omit<
30
+ CalcFlowData,
31
+ 'node' | 'mode' | 'onRequestPicker' | 'onRequestMemberPicker'
32
+ >;
33
+ applyFocus: (id: string | null) => void;
34
+ applyEngaged: (id: string | null) => void;
35
+ centerOnNode: (node: AnyCalcFlowNode) => void;
36
+ setPickerNode: (node: CalcNode | null) => void;
37
+ }) {
38
+ const { screenToFlowPosition, getNode } = useReactFlow();
39
+
40
+ const handleNodeEnter = useCallback(
41
+ (_event: MouseEvent, node: AnyCalcFlowNode) => applyEngaged(node.id),
42
+ [applyEngaged],
43
+ );
44
+ const handleNodeLeave = useCallback(
45
+ () => applyEngaged(null),
46
+ [applyEngaged],
47
+ );
48
+ const handleNodeClick = useCallback(
49
+ (_event: MouseEvent, node: AnyCalcFlowNode) => {
50
+ applyFocus(node.id);
51
+ centerOnNode(node);
52
+ },
53
+ [applyFocus, centerOnNode],
54
+ );
55
+
56
+ const handleNodeDoubleClick = useCallback(
57
+ (event: MouseEvent, node: AnyCalcFlowNode) => {
58
+ const el = event.target as HTMLElement | null;
59
+ if (
60
+ el?.closest(
61
+ 'button, a, input, [role="button"], .nodrag, .calc-port',
62
+ )
63
+ ) {
64
+ return;
65
+ }
66
+ const target = (node.data as CalcFlowData).node;
67
+ if (!target) return;
68
+
69
+ if (target.recipe != null || target.availableRecipes.length > 0)
70
+ setPickerNode(target);
71
+ },
72
+ [setPickerNode],
73
+ );
74
+ const handlePaneClick = useCallback(
75
+ () => applyFocus(null),
76
+ [applyFocus],
77
+ );
78
+ const handleDragOver = useCallback((event: DragEvent) => {
79
+ if (event.dataTransfer.types.includes('application/x-tfg-calc')) {
80
+ event.preventDefault();
81
+ event.dataTransfer.dropEffect = 'copy';
82
+ }
83
+ }, []);
84
+ const handleDrop = useCallback(
85
+ (event: DragEvent) => {
86
+ const raw = event.dataTransfer.getData('application/x-tfg-calc');
87
+ if (!raw) return;
88
+ event.preventDefault();
89
+ try {
90
+ const parsed = JSON.parse(raw) as { id?: string; kind?: CalcKind };
91
+ if (parsed.id && (parsed.kind === 'item' || parsed.kind === 'fluid')) {
92
+ const pos = screenToFlowPosition({
93
+ x: event.clientX,
94
+ y: event.clientY,
95
+ });
96
+ data.onAddCard(parsed.id, parsed.kind, pos);
97
+ }
98
+ } catch {}
99
+ },
100
+ [screenToFlowPosition, data],
101
+ );
102
+
103
+ const fitRecipeNodes = useCallback(
104
+ (node: AnyCalcFlowNode) => node.data.node?.recipe != null,
105
+ [],
106
+ );
107
+
108
+ const handleConnect = useCallback(
109
+ (conn: Connection) => {
110
+ const { source, target, sourceHandle, targetHandle } = conn;
111
+ if (!source || !target || source === target) return;
112
+ const input = Number(/^in-(\d+)$/.exec(targetHandle ?? '')?.[1] ?? -1);
113
+ const outMatch = /^out(?:-(\d+))?$/.exec(sourceHandle ?? '');
114
+ if (input < 0 || !outMatch) return;
115
+ const out = Number(outMatch[1] ?? 0);
116
+ const sNode = (getNode(source)?.data as CalcFlowData | undefined)?.node;
117
+ const tNode = (getNode(target)?.data as CalcFlowData | undefined)?.node;
118
+ const slot = tNode?.inputs[input];
119
+ const srcOut = sNode?.outputs[out];
120
+ if (!sNode || !tNode || !slot || !srcOut) return;
121
+ if (slot.id !== srcOut.id || slot.kind !== srcOut.kind) return;
122
+ data.onWire(source, target, input, out);
123
+ },
124
+ [data, getNode],
125
+ );
126
+
127
+ const handleReconnect = useCallback(
128
+ (oldEdge: Edge, conn: Connection) => {
129
+ const wire = oldEdge.data?.wire as
130
+ | { from: string; to: string; input: number; out?: number }
131
+ | undefined;
132
+ const input = Number(/^in-(\d+)$/.exec(conn.targetHandle ?? '')?.[1] ?? -1);
133
+ const outMatch = /^out(?:-(\d+))?$/.exec(conn.sourceHandle ?? '');
134
+ if (!wire || !conn.source || !conn.target || input < 0 || !outMatch)
135
+ return;
136
+ const out = Number(outMatch[1] ?? 0);
137
+ if (conn.source === conn.target) return;
138
+ const sNode = (getNode(conn.source)?.data as CalcFlowData | undefined)?.node;
139
+ const tNode = (getNode(conn.target)?.data as CalcFlowData | undefined)?.node;
140
+ const slot = tNode?.inputs[input];
141
+ const srcOut = sNode?.outputs[out];
142
+ if (!sNode || !tNode || !slot || !srcOut) return;
143
+ if (slot.id !== srcOut.id || slot.kind !== srcOut.kind) return;
144
+ data.onRewire(wire.from, wire.to, wire.input, {
145
+ from: conn.source,
146
+ to: conn.target,
147
+ input,
148
+ out,
149
+ });
150
+ },
151
+ [data, getNode],
152
+ );
153
+
154
+ const handleEdgesDelete = useCallback(
155
+ (edges: Edge[]) => {
156
+ for (const edge of edges) {
157
+ const wire = edge.data?.wire as
158
+ | { from: string; to: string; input: number; out?: number }
159
+ | undefined;
160
+ if (wire) data.onUnwire(wire.from, wire.to, wire.input, wire.out);
161
+ }
162
+ },
163
+ [data],
164
+ );
165
+
166
+ const handleEdgeClick = useCallback(
167
+ (event: MouseEvent, edge: Edge) => {
168
+ if (!event.shiftKey) return;
169
+ const wire = edge.data?.wire as
170
+ | { from: string; to: string; input: number; out?: number }
171
+ | undefined;
172
+ if (wire) data.onUnwire(wire.from, wire.to, wire.input, wire.out);
173
+ },
174
+ [data],
175
+ );
176
+
177
+ return {
178
+ handleNodeEnter,
179
+ handleNodeLeave,
180
+ handleNodeClick,
181
+ handleNodeDoubleClick,
182
+ handlePaneClick,
183
+ handleDragOver,
184
+ handleDrop,
185
+ handleConnect,
186
+ handleReconnect,
187
+ handleEdgesDelete,
188
+ handleEdgeClick,
189
+ fitRecipeNodes,
190
+ };
191
+ }
192
+
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+ import {
5
+ cardsToFlow,
6
+ GRID_GAPS,
7
+ CALC_NODE_H,
8
+ CALC_NODE_W,
9
+ type CalcFlowData,
10
+ type CalcViewMode,
11
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
12
+ import { layoutCards } from '@terrafirmagreg-team/recipe/lib/calc/pack';
13
+ import type {
14
+ CalcCard,
15
+ CalcNode,
16
+ CalcWire,
17
+ } from '@terrafirmagreg-team/recipe/types/calc';
18
+
19
+ export function useFlowLayout({
20
+ nodes,
21
+ wires,
22
+ flows,
23
+ cards,
24
+ data,
25
+ mode,
26
+ onRequestPicker,
27
+ onRequestMemberPicker,
28
+ onRequestSupply,
29
+ }: {
30
+ nodes: CalcNode[];
31
+ wires: CalcWire[];
32
+ flows?: readonly (number | undefined)[];
33
+ cards: CalcCard[];
34
+ data: Omit<
35
+ CalcFlowData,
36
+ | 'node'
37
+ | 'mode'
38
+ | 'onRequestPicker'
39
+ | 'onRequestMemberPicker'
40
+ | 'onRequestSupply'
41
+ | 'onCrafts'
42
+ >;
43
+ mode: CalcViewMode;
44
+ onRequestPicker: (node: CalcNode) => void;
45
+ onRequestMemberPicker: (node: CalcNode) => void;
46
+ onRequestSupply: (node: CalcNode, input: number) => void;
47
+ }) {
48
+ const flowData = useMemo(
49
+ () =>
50
+ ({
51
+ ...data,
52
+ mode,
53
+ onRequestPicker,
54
+ onRequestMemberPicker,
55
+ onRequestSupply,
56
+ }) as Omit<CalcFlowData, 'node'>,
57
+ [
58
+ data,
59
+ mode,
60
+ onRequestPicker,
61
+ onRequestMemberPicker,
62
+ onRequestSupply,
63
+ ],
64
+ );
65
+ const rawFlow = useMemo(
66
+ () => cardsToFlow(nodes, wires, flowData, flows),
67
+ [nodes, wires, flowData, flows],
68
+ );
69
+
70
+ const storedPos = useMemo(() => {
71
+ const map = new Map<string, { x: number; y: number }>();
72
+ for (const card of cards) {
73
+ if (card.pos) map.set(card.id, card.pos);
74
+ }
75
+ return map;
76
+ }, [cards]);
77
+
78
+ const positioned = useMemo(() => {
79
+ const flowNodes = rawFlow.nodes.map((node, index) => {
80
+ const stored = storedPos.get(node.id);
81
+ const position = stored ?? {
82
+ x: 40 + (index % 4) * (CALC_NODE_W + GRID_GAPS.x + 20),
83
+ y: 40 + Math.floor(index / 4) * (CALC_NODE_H + GRID_GAPS.y + 40),
84
+ };
85
+ return { ...node, position };
86
+ });
87
+ const present = new Set(flowNodes.map((node) => node.id));
88
+ const edges = rawFlow.edges.filter(
89
+ (edge) => present.has(edge.source) && present.has(edge.target),
90
+ );
91
+ return { nodes: flowNodes, edges };
92
+ }, [rawFlow, storedPos]);
93
+
94
+ const relayout = useCallback(
95
+ () => layoutCards(nodes, wires, {
96
+ mode,
97
+ gaps: GRID_GAPS,
98
+ recipeBody: data.recipeBody,
99
+ }),
100
+ [nodes, wires, mode, data.recipeBody],
101
+ );
102
+
103
+ return { rawFlow, positioned, relayout };
104
+ }
105
+
@@ -94,3 +94,4 @@ export function useMountedNodes(computed: {
94
94
  clearMounts: () => setPendingMounts([]),
95
95
  };
96
96
  }
97
+
@@ -9,24 +9,24 @@ import {
9
9
  type RefObject,
10
10
  } from 'react';
11
11
  import {
12
+ applyEdgeChanges,
12
13
  applyNodeChanges,
13
14
  useUpdateNodeInternals,
15
+ type EdgeChange,
14
16
  type NodeChange,
15
17
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
16
18
  import {
17
19
  type AnyCalcFlowNode,
18
- type CalcDirection,
19
- type CalcEdgeStyle,
20
20
  type CalcFlowData,
21
21
  type CalcViewMode,
22
22
  } from '@terrafirmagreg-team/recipe/lib/calc/flow';
23
23
  import { createCircuitRouter } from '@terrafirmagreg-team/recipe/lib/calc/route';
24
- import { useFlowWindow } from './window';
25
- import { useMountedNodes } from './mount';
26
- import { useEdgeEngage } from './engage';
27
- import type { goalsToFlow } from '@terrafirmagreg-team/recipe/lib/calc/flow';
24
+ import { useFlowWindow } from './use-window';
25
+ import { useMountedNodes } from './use-mount';
26
+ import { useEdgeEngage } from './use-engage';
27
+ import type { cardsToFlow } from '@terrafirmagreg-team/recipe/lib/calc/flow';
28
28
 
29
- type Positioned = ReturnType<typeof goalsToFlow>;
29
+ type Positioned = ReturnType<typeof cardsToFlow>;
30
30
 
31
31
  export function useFlowNodes({
32
32
  positioned,
@@ -36,22 +36,18 @@ export function useFlowNodes({
36
36
  focusIdRef,
37
37
  rawFlow,
38
38
  mode,
39
- direction,
40
- edgeStyle,
41
39
  canvasId,
42
40
  }: {
43
41
  positioned: Positioned;
44
42
  shift: { x: number; y: number } | null;
45
43
  data: Omit<
46
44
  CalcFlowData,
47
- 'node' | 'mode' | 'direction' | 'onRequestPicker' | 'onRequestMemberPicker'
45
+ 'node' | 'mode' | 'onRequestPicker' | 'onRequestMemberPicker'
48
46
  >;
49
47
  focusId: string | null;
50
48
  focusIdRef: RefObject<string | null>;
51
49
  rawFlow: Positioned;
52
50
  mode: CalcViewMode;
53
- direction: CalcDirection;
54
- edgeStyle: CalcEdgeStyle;
55
51
  canvasId?: string;
56
52
  }) {
57
53
  const [movedNodes, setMovedNodes] = useState<Map<
@@ -72,11 +68,17 @@ export function useFlowNodes({
72
68
  () => new Set(),
73
69
  );
74
70
  const [routeCircuit] = useState(createCircuitRouter);
71
+ const [liveMoved, setLiveMoved] = useState<Map<
72
+ string,
73
+ { x: number; y: number }
74
+ > | null>(null);
75
75
  const placed = useMemo(
76
76
  () =>
77
77
  positioned.nodes.map((node) => {
78
- const moved = movedNodes?.get(node.id);
79
- const position = moved ?? node.position;
78
+ const position =
79
+ liveMoved?.get(node.id) ??
80
+ movedNodes?.get(node.id) ??
81
+ node.position;
80
82
  return {
81
83
  ...node,
82
84
  position: shift
@@ -84,15 +86,20 @@ export function useFlowNodes({
84
86
  : position,
85
87
  };
86
88
  }),
87
- [positioned, movedNodes, shift],
89
+ [positioned, movedNodes, liveMoved, shift],
88
90
  );
89
91
 
90
92
  const routes = useMemo(
91
93
  () =>
92
- edgeStyle === 'circuit'
93
- ? routeCircuit(placed, positioned.edges, direction)
94
- : null,
95
- [placed, positioned.edges, direction, edgeStyle, routeCircuit],
94
+ routeCircuit(
95
+ placed.map((node) => ({
96
+ ...node,
97
+ inputs: node.data?.node?.inputs?.length ?? 0,
98
+ outputs: node.data?.node?.outputs?.length ?? 1,
99
+ })),
100
+ positioned.edges,
101
+ ),
102
+ [placed, positioned.edges, routeCircuit],
96
103
  );
97
104
  const mergedFlow = useMemo(() => {
98
105
  const nodes = placed.map((node) => ({
@@ -101,15 +108,11 @@ export function useFlowNodes({
101
108
  data: { ...node.data, ...data },
102
109
  }));
103
110
  const edges = positioned.edges.map((edge) => {
104
-
105
- if (draggedIds.has(edge.source) || draggedIds.has(edge.target)) {
106
- return edge;
107
- }
108
111
  const points = routes?.get(edge.id);
109
112
  return points ? { ...edge, data: { ...edge.data, points } } : edge;
110
113
  });
111
114
  return { nodes, edges };
112
- }, [placed, positioned.edges, data, focusId, routes, draggedIds]);
115
+ }, [placed, positioned.edges, data, focusId, routes]);
113
116
  const { computed, extraRects } = useFlowWindow({
114
117
  mergedFlow,
115
118
  focusId,
@@ -148,16 +151,9 @@ export function useFlowNodes({
148
151
  if (changed.length) updateNodeInternals(changed);
149
152
  }, [nodes, updateNodeInternals]);
150
153
 
151
- const directionRef = useRef(direction);
152
- useEffect(() => {
153
- if (directionRef.current === direction) return;
154
- directionRef.current = direction;
155
- updateNodeInternals(nodes.map((node) => node.id));
156
- }, [direction, nodes, updateNodeInternals]);
157
-
158
- const [prevLayout, setPrevLayout] = useState({ mode, direction });
159
- if (prevLayout.mode !== mode || prevLayout.direction !== direction) {
160
- setPrevLayout({ mode, direction });
154
+ const [prevLayout, setPrevLayout] = useState(mode);
155
+ if (prevLayout !== mode) {
156
+ setPrevLayout(mode);
161
157
  setMovedNodes(null);
162
158
  setDraggedIds(new Set());
163
159
  }
@@ -165,7 +161,7 @@ export function useFlowNodes({
165
161
  movedLiveRef.current = null;
166
162
  dragIdsRef.current.clear();
167
163
  pendingChangesRef.current = [];
168
- }, [mode, direction, canvasId]);
164
+ }, [mode, canvasId]);
169
165
 
170
166
  const flushChanges = useCallback(() => {
171
167
  changesFrameRef.current = null;
@@ -182,8 +178,9 @@ export function useFlowNodes({
182
178
  if (change.type !== 'position' || change.dragging) continue;
183
179
  if (!change.position || !dragIdsRef.current.delete(change.id)) continue;
184
180
  dragEnds.push({ id: change.id, ...change.position });
181
+ data.onMoveCard(change.id, change.position);
185
182
  }
186
-
183
+ setLiveMoved(live?.size ? new Map(live) : null);
187
184
  if (!dragEnds.length) return;
188
185
  setDraggedIds(new Set(dragIdsRef.current));
189
186
  movedLiveRef.current = null;
@@ -193,7 +190,7 @@ export function useFlowNodes({
193
190
  for (const end of dragEnds) next.set(end.id, { x: end.x, y: end.y });
194
191
  return next;
195
192
  });
196
- }, [setState]);
193
+ }, [setState, data]);
197
194
  useEffect(
198
195
  () => () => {
199
196
  if (changesFrameRef.current != null)
@@ -224,6 +221,16 @@ export function useFlowNodes({
224
221
  [flushChanges],
225
222
  );
226
223
 
224
+ const onEdgesChange = useCallback(
225
+ (changes: EdgeChange[]) => {
226
+ setState((prev) => ({
227
+ ...prev,
228
+ edges: applyEdgeChanges(changes, prev.edges),
229
+ }));
230
+ },
231
+ [setState],
232
+ );
233
+
227
234
  const clearMoved = useCallback(() => {
228
235
  movedLiveRef.current = null;
229
236
  setMovedNodes(null);
@@ -245,7 +252,9 @@ export function useFlowNodes({
245
252
  clearMoved,
246
253
  setMovedNodes,
247
254
  onNodesChange,
255
+ onEdgesChange,
248
256
  applyEngaged,
249
257
  resetNodes,
250
258
  };
251
259
  }
260
+
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import type { CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
5
+
6
+ export type SupplyRequest = { node: CalcNode; input: number };
7
+
8
+ export function useFlowPickers(nodes: CalcNode[]) {
9
+ const [pickerNode, setPickerNode] = useState<CalcNode | null>(null);
10
+ const [memberNode, setMemberNode] = useState<CalcNode | null>(null);
11
+ const [supply, setSupply] = useState<SupplyRequest | null>(null);
12
+ const onRequestPicker = useCallback(
13
+ (node: CalcNode) => setPickerNode(node),
14
+ [],
15
+ );
16
+ const onRequestMemberPicker = useCallback(
17
+ (node: CalcNode) => setMemberNode(node),
18
+ [],
19
+ );
20
+ const onRequestSupply = useCallback(
21
+ (node: CalcNode, input: number) => setSupply({ node, input }),
22
+ [],
23
+ );
24
+
25
+ const [prevNodes, setPrevNodes] = useState(nodes);
26
+ if (prevNodes !== nodes) {
27
+ setPrevNodes(nodes);
28
+ for (const [node, set] of [
29
+ [pickerNode, setPickerNode],
30
+ [memberNode, setMemberNode],
31
+ ] as const) {
32
+ if (!node) continue;
33
+ const live = nodes.find((n) => n.id === node.id);
34
+ if (live && live !== node) set(live);
35
+ else if (!live) set(null);
36
+ }
37
+ if (supply) {
38
+ const live = nodes.find((n) => n.id === supply.node.id);
39
+ if (!live) setSupply(null);
40
+ else if (live !== supply.node)
41
+ setSupply({ node: live, input: supply.input });
42
+ }
43
+ }
44
+
45
+ return {
46
+ pickerNode,
47
+ setPickerNode,
48
+ memberNode,
49
+ setMemberNode,
50
+ supply,
51
+ setSupply,
52
+ onRequestPicker,
53
+ onRequestMemberPicker,
54
+ onRequestSupply,
55
+ };
56
+ }
57
+