@terrafirmagreg-team/recipe 0.0.0-stage → 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 (221) hide show
  1. package/package.json +62 -3
  2. package/src/components/button-copy.tsx +43 -0
  3. package/src/components/cards/card/index.tsx +214 -0
  4. package/src/components/cards/card/scene.tsx +107 -0
  5. package/src/components/cards/card/strip.tsx +198 -0
  6. package/src/components/cards/card/text.tsx +46 -0
  7. package/src/components/cards/card/tips.tsx +63 -0
  8. package/src/components/cards/default-item.tsx +47 -0
  9. package/src/components/cards/glare-filter.tsx +29 -0
  10. package/src/components/cards/paged-card.tsx +222 -0
  11. package/src/components/cards/slot-hitbox.tsx +213 -0
  12. package/src/components/cards/strip-card/buttons.tsx +106 -0
  13. package/src/components/cards/strip-card/index.tsx +214 -0
  14. package/src/components/cards/strip-card/member-cell.tsx +167 -0
  15. package/src/components/cards/strip-card/sprite.tsx +36 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +129 -0
  18. package/src/components/tool-list.tsx +49 -0
  19. package/src/components/tree.tsx +98 -0
  20. package/src/components/views/alloy/index.tsx +112 -0
  21. package/src/components/views/alloy/list.tsx +85 -0
  22. package/src/components/views/alloy/mix/index.tsx +35 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +94 -0
  24. package/src/components/views/alloy/mix/rows.tsx +145 -0
  25. package/src/components/views/alloy/mix/status.tsx +122 -0
  26. package/src/components/views/alloy/mix/total.tsx +132 -0
  27. package/src/components/views/alloy/range-bar.tsx +65 -0
  28. package/src/components/views/alloy/stack-bar.tsx +88 -0
  29. package/src/components/views/browser/axis-switcher.tsx +54 -0
  30. package/src/components/views/browser/card-grid.tsx +275 -0
  31. package/src/components/views/browser/favorite-button.tsx +46 -0
  32. package/src/components/views/browser/frame.tsx +112 -0
  33. package/src/components/views/browser/index.tsx +114 -0
  34. package/src/components/views/browser/item-panel/index.tsx +229 -0
  35. package/src/components/views/browser/item-panel/skeleton.tsx +26 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +178 -0
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +129 -0
  38. package/src/components/views/browser/item-panel-grid/index.tsx +105 -0
  39. package/src/components/views/browser/item-view/index.tsx +186 -0
  40. package/src/components/views/browser/item-view/tags.tsx +85 -0
  41. package/src/components/views/browser/item-view-header.tsx +131 -0
  42. package/src/components/views/browser/pane-tabs.tsx +61 -0
  43. package/src/components/views/browser/place-header.tsx +72 -0
  44. package/src/components/views/browser/query-controls.tsx +105 -0
  45. package/src/components/views/browser/recipe-item-header.tsx +60 -0
  46. package/src/components/views/browser/settings.tsx +52 -0
  47. package/src/components/views/browser/toolbar.tsx +54 -0
  48. package/src/components/views/browser/type-tabs.tsx +71 -0
  49. package/src/components/views/browser/type-view.tsx +184 -0
  50. package/src/components/views/browser/workstation-rail.tsx +43 -0
  51. package/src/components/views/calc/calculator/actions.tsx +47 -0
  52. package/src/components/views/calc/calculator/help.tsx +189 -0
  53. package/src/components/views/calc/calculator/index.tsx +172 -0
  54. package/src/components/views/calc/dot-overlay.tsx +80 -0
  55. package/src/components/views/calc/edge.tsx +152 -0
  56. package/src/components/views/calc/flow/index.tsx +305 -0
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +146 -0
  58. package/src/components/views/calc/flow/toolbar.tsx +60 -0
  59. package/src/components/views/calc/flow/view-menu.tsx +117 -0
  60. package/src/components/views/calc/member-picker.tsx +120 -0
  61. package/src/components/views/calc/node-card/actions.tsx +90 -0
  62. package/src/components/views/calc/node-card/badges.tsx +59 -0
  63. package/src/components/views/calc/node-card/chips.tsx +187 -0
  64. package/src/components/views/calc/node-card/far.tsx +123 -0
  65. package/src/components/views/calc/node-card/full.tsx +332 -0
  66. package/src/components/views/calc/node-card/index.tsx +27 -0
  67. package/src/components/views/calc/node-card/menu.tsx +282 -0
  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 +207 -0
  72. package/src/components/views/calc/recipe-picker/footer.tsx +131 -0
  73. package/src/components/views/calc/recipe-picker/index.tsx +52 -0
  74. package/src/components/views/calc/sidebar.tsx +171 -0
  75. package/src/components/views/calc/trees/icon-picker.tsx +97 -0
  76. package/src/components/views/calc/trees/index.tsx +134 -0
  77. package/src/components/views/calc/trees/label.tsx +67 -0
  78. package/src/components/views/calc/trees/rows.tsx +140 -0
  79. package/src/components/views/forge/gui.tsx +131 -0
  80. package/src/components/views/forge/index.tsx +112 -0
  81. package/src/components/views/forge/list.tsx +113 -0
  82. package/src/components/views/forge/workbench/controls.tsx +103 -0
  83. package/src/components/views/forge/workbench/index.tsx +85 -0
  84. package/src/components/views/forge/workbench/panel.tsx +228 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +133 -0
  86. package/src/components/views/forge/workbench/status.tsx +55 -0
  87. package/src/components/views/forge/workbench/tape.tsx +70 -0
  88. package/src/hooks/alloy/use-fluid-colors.ts +125 -0
  89. package/src/hooks/alloy/use-mix-state.ts +240 -0
  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/use-mount.ts +97 -0
  94. package/src/hooks/calc/flow/use-nodes.ts +260 -0
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/use-viewport.ts +141 -0
  97. package/src/hooks/calc/flow/use-window.ts +87 -0
  98. package/src/hooks/calc/use-actions.ts +355 -0
  99. package/src/hooks/calc/use-amounts.ts +64 -0
  100. package/src/hooks/calc/use-data.ts +203 -0
  101. package/src/hooks/calc/use-defaults.ts +40 -0
  102. package/src/hooks/calc/use-far-mode.ts +37 -0
  103. package/src/hooks/calc/use-focus.ts +31 -0
  104. package/src/hooks/calc/use-maps.ts +76 -0
  105. package/src/hooks/calc/use-reset.ts +15 -0
  106. package/src/hooks/calc/use-resolver.ts +70 -0
  107. package/src/hooks/calc/use-session.ts +13 -0
  108. package/src/hooks/calc/use-share.ts +64 -0
  109. package/src/hooks/calc/use-storage.ts +126 -0
  110. package/src/hooks/calc/use-tier.ts +13 -0
  111. package/src/hooks/calc/use-view-mode.ts +24 -0
  112. package/src/hooks/calc/use-worker.ts +204 -0
  113. package/src/hooks/forge/use-anvil-gui.ts +16 -0
  114. package/src/hooks/forge/use-forge-state.ts +135 -0
  115. package/src/hooks/forge/use-ruler.ts +100 -0
  116. package/src/hooks/use-add-to-calc.ts +51 -0
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +39 -0
  119. package/src/hooks/use-favorites.ts +135 -0
  120. package/src/hooks/use-group-outlines.ts +65 -0
  121. package/src/hooks/use-hide-unused.ts +12 -0
  122. package/src/hooks/use-index.ts +72 -0
  123. package/src/hooks/use-item-feeds.ts +46 -0
  124. package/src/hooks/use-item-pane.ts +106 -0
  125. package/src/hooks/use-item-press.ts +89 -0
  126. package/src/hooks/use-item-tag-groups.ts +54 -0
  127. package/src/hooks/use-item-tooltips.ts +18 -0
  128. package/src/hooks/use-left-sidebar-tree.ts +34 -0
  129. package/src/hooks/use-mod-names.ts +17 -0
  130. package/src/hooks/use-mount-limit.ts +22 -0
  131. package/src/hooks/use-panel-cells.ts +117 -0
  132. package/src/hooks/use-panel-page.ts +54 -0
  133. package/src/hooks/use-panel-tabs.ts +43 -0
  134. package/src/hooks/use-query.ts +186 -0
  135. package/src/hooks/use-session.ts +88 -0
  136. package/src/hooks/use-sidebar-group.ts +16 -0
  137. package/src/hooks/use-tag-members.ts +16 -0
  138. package/src/hooks/use-tool-recipes.ts +26 -0
  139. package/src/hooks/use-type-filter.ts +73 -0
  140. package/src/lib/alloy/mix.ts +210 -0
  141. package/src/lib/alloy/parse.ts +37 -0
  142. package/src/lib/calc/defaults.ts +47 -0
  143. package/src/lib/calc/flow.ts +315 -0
  144. package/src/lib/calc/focus.ts +15 -0
  145. package/src/lib/calc/meta.ts +17 -0
  146. package/src/lib/calc/pack.ts +77 -0
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +198 -0
  149. package/src/lib/calc/route/buckets.ts +49 -0
  150. package/src/lib/calc/route/geometry.ts +51 -0
  151. package/src/lib/calc/route/index.ts +3 -0
  152. package/src/lib/calc/route/polyline.ts +103 -0
  153. package/src/lib/calc/route/router.ts +394 -0
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +97 -0
  156. package/src/lib/calc/storage/index.ts +20 -0
  157. package/src/lib/calc/storage/keys.ts +38 -0
  158. package/src/lib/calc/storage/store.ts +198 -0
  159. package/src/lib/calc/tree/index.ts +4 -0
  160. package/src/lib/calc/tree/materials.ts +29 -0
  161. package/src/lib/calc/tree/recipe.ts +104 -0
  162. package/src/lib/calc/tree/share.ts +135 -0
  163. package/src/lib/calc/worker/api.ts +25 -0
  164. package/src/lib/calc/worker/fetch.ts +66 -0
  165. package/src/lib/calc/worker/flush.ts +39 -0
  166. package/src/lib/calc/worker/index.ts +8 -0
  167. package/src/lib/calc/worker/protocol.ts +35 -0
  168. package/src/lib/calc/worker/resolve.ts +49 -0
  169. package/src/lib/calc/worker/state.ts +48 -0
  170. package/src/lib/card-model.ts +65 -0
  171. package/src/lib/client/recipe-categories.ts +14 -0
  172. package/src/lib/client/recipe.ts +85 -0
  173. package/src/lib/client-index/catalog.ts +91 -0
  174. package/src/lib/client-index/chrome.ts +180 -0
  175. package/src/lib/client-index/index.ts +20 -0
  176. package/src/lib/client-index/registry.ts +121 -0
  177. package/src/lib/client-index/rows.ts +260 -0
  178. package/src/lib/client-index/tooltips.ts +74 -0
  179. package/src/lib/config.ts +18 -0
  180. package/src/lib/data/category/dto.ts +25 -0
  181. package/src/lib/data/category/load.ts +47 -0
  182. package/src/lib/data/category/parse.ts +71 -0
  183. package/src/lib/data/dto.ts +152 -0
  184. package/src/lib/data/grid.ts +40 -0
  185. package/src/lib/data/parse/entry.ts +85 -0
  186. package/src/lib/data/parse/index.ts +3 -0
  187. package/src/lib/data/parse/layout.ts +138 -0
  188. package/src/lib/data/parse/slots.ts +85 -0
  189. package/src/lib/data/parse/widgets.ts +81 -0
  190. package/src/lib/data/scene.ts +103 -0
  191. package/src/lib/data/slot-io.ts +44 -0
  192. package/src/lib/dump.ts +6 -0
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +13 -0
  195. package/src/lib/fluid-outputs.ts +70 -0
  196. package/src/lib/forge/actions.ts +59 -0
  197. package/src/lib/forge/data.ts +51 -0
  198. package/src/lib/forge/solve.ts +80 -0
  199. package/src/lib/group.ts +23 -0
  200. package/src/lib/index-store.ts +70 -0
  201. package/src/lib/links.ts +131 -0
  202. package/src/lib/lookup.ts +119 -0
  203. package/src/lib/panel-outline.ts +86 -0
  204. package/src/lib/panel.ts +105 -0
  205. package/src/lib/preview.ts +32 -0
  206. package/src/lib/recipe-slots.ts +8 -0
  207. package/src/lib/search.ts +139 -0
  208. package/src/lib/sidebar.ts +47 -0
  209. package/src/lib/used-items.ts +25 -0
  210. package/src/locales/en_us.json +172 -0
  211. package/src/locales/ru_ru.json +172 -0
  212. package/src/locales.ts +6 -0
  213. package/src/types/alloy.ts +20 -0
  214. package/src/types/calc-flow-props.ts +44 -0
  215. package/src/types/calc.ts +92 -0
  216. package/src/types/forge.ts +21 -0
  217. package/src/types/index.ts +136 -0
  218. package/styles/calc.css +308 -0
  219. package/styles/strip.css +7 -0
  220. package/styles.css +2 -0
  221. package/README.md +0 -3
@@ -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
+
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import {
5
+ reconcileCalcEdges,
6
+ reconcileCalcNodes,
7
+ type AnyCalcFlowNode,
8
+ type CalcFlowNode,
9
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
10
+ import type { Edge } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
11
+
12
+ export type MountedNodes = {
13
+ raw: { nodes: AnyCalcFlowNode[]; edges: Edge[] };
14
+ nodes: AnyCalcFlowNode[];
15
+ edges: Edge[];
16
+ };
17
+
18
+ export function useMountedNodes(computed: {
19
+ nodes: AnyCalcFlowNode[];
20
+ edges: Edge[];
21
+ }) {
22
+ const [pendingMounts, setPendingMounts] = useState<AnyCalcFlowNode[]>([]);
23
+ const computedRef = useRef(computed);
24
+ const [state, setState] = useState<MountedNodes>(() => ({
25
+ raw: computed,
26
+
27
+ nodes: reconcileCalcNodes([], computed.nodes),
28
+ edges: computed.edges,
29
+ }));
30
+ useEffect(() => {
31
+ computedRef.current = computed;
32
+ });
33
+ if (state.raw !== computed) {
34
+ const prevIds = new Set(state.nodes.map((n) => n.id));
35
+ const fresh = computed.nodes.filter((n) => !prevIds.has(n.id));
36
+ const first = new Set(fresh.slice(0, 200).map((n) => n.id));
37
+ const rest = fresh.slice(200);
38
+ if (rest.length !== pendingMounts.length || rest.length === 0) {
39
+ setPendingMounts(rest);
40
+ }
41
+ setState({
42
+ raw: computed,
43
+ nodes: reconcileCalcNodes(
44
+ state.nodes,
45
+ computed.nodes.filter(
46
+ (n) => prevIds.has(n.id) || first.has(n.id),
47
+ ),
48
+ ),
49
+ edges: reconcileCalcEdges(state.edges, computed.edges),
50
+ });
51
+ }
52
+ useEffect(() => {
53
+ if (!pendingMounts.length) return;
54
+ const raf = requestAnimationFrame(() => {
55
+ const alive = new Set(
56
+ computedRef.current.nodes.map((n) => n.id),
57
+ );
58
+ const slice: AnyCalcFlowNode[] = [];
59
+ const rest: AnyCalcFlowNode[] = [];
60
+ const start = performance.now();
61
+ for (const node of pendingMounts) {
62
+ if (!alive.has(node.id)) continue;
63
+ if (slice.length >= 24 && performance.now() - start > 24) {
64
+ rest.push(node);
65
+ } else {
66
+ slice.push(node);
67
+ }
68
+ }
69
+ setPendingMounts(rest);
70
+ if (!slice.length) return;
71
+ setState((prev) => {
72
+ const have = new Set(prev.nodes.map((n) => n.id));
73
+ const add = slice
74
+ .filter((n) => !have.has(n.id))
75
+ .map((node, i) => ({
76
+ ...node,
77
+ style: {
78
+ ...node.style,
79
+ '--calc-delay': `${Math.min((i + 1) * 14, 300)}ms`,
80
+ } as CalcFlowNode['style'],
81
+ }));
82
+ return add.length
83
+ ? { ...prev, nodes: [...prev.nodes, ...add] }
84
+ : prev;
85
+ });
86
+ });
87
+ return () => cancelAnimationFrame(raf);
88
+ }, [pendingMounts]);
89
+
90
+ return {
91
+ nodes: state.nodes,
92
+ edges: state.edges,
93
+ setState,
94
+ clearMounts: () => setPendingMounts([]),
95
+ };
96
+ }
97
+
@@ -0,0 +1,260 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type RefObject,
10
+ } from 'react';
11
+ import {
12
+ applyEdgeChanges,
13
+ applyNodeChanges,
14
+ useUpdateNodeInternals,
15
+ type EdgeChange,
16
+ type NodeChange,
17
+ } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
18
+ import {
19
+ type AnyCalcFlowNode,
20
+ type CalcFlowData,
21
+ type CalcViewMode,
22
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
23
+ import { createCircuitRouter } from '@terrafirmagreg-team/recipe/lib/calc/route';
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
+
29
+ type Positioned = ReturnType<typeof cardsToFlow>;
30
+
31
+ export function useFlowNodes({
32
+ positioned,
33
+ shift,
34
+ data,
35
+ focusId,
36
+ focusIdRef,
37
+ rawFlow,
38
+ mode,
39
+ canvasId,
40
+ }: {
41
+ positioned: Positioned;
42
+ shift: { x: number; y: number } | null;
43
+ data: Omit<
44
+ CalcFlowData,
45
+ 'node' | 'mode' | 'onRequestPicker' | 'onRequestMemberPicker'
46
+ >;
47
+ focusId: string | null;
48
+ focusIdRef: RefObject<string | null>;
49
+ rawFlow: Positioned;
50
+ mode: CalcViewMode;
51
+ canvasId?: string;
52
+ }) {
53
+ const [movedNodes, setMovedNodes] = useState<Map<
54
+ string,
55
+ { x: number; y: number }
56
+ > | null>(null);
57
+
58
+ const movedLiveRef = useRef<Map<string, { x: number; y: number }> | null>(
59
+ null,
60
+ );
61
+
62
+ const pendingChangesRef = useRef<NodeChange<AnyCalcFlowNode>[]>([]);
63
+ const changesFrameRef = useRef<number | null>(null);
64
+
65
+ const dragIdsRef = useRef(new Set<string>());
66
+
67
+ const [draggedIds, setDraggedIds] = useState<ReadonlySet<string>>(
68
+ () => new Set(),
69
+ );
70
+ const [routeCircuit] = useState(createCircuitRouter);
71
+ const [liveMoved, setLiveMoved] = useState<Map<
72
+ string,
73
+ { x: number; y: number }
74
+ > | null>(null);
75
+ const placed = useMemo(
76
+ () =>
77
+ positioned.nodes.map((node) => {
78
+ const position =
79
+ liveMoved?.get(node.id) ??
80
+ movedNodes?.get(node.id) ??
81
+ node.position;
82
+ return {
83
+ ...node,
84
+ position: shift
85
+ ? { x: position.x + shift.x, y: position.y + shift.y }
86
+ : position,
87
+ };
88
+ }),
89
+ [positioned, movedNodes, liveMoved, shift],
90
+ );
91
+
92
+ const routes = useMemo(
93
+ () =>
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],
103
+ );
104
+ const mergedFlow = useMemo(() => {
105
+ const nodes = placed.map((node) => ({
106
+ ...node,
107
+ className: node.id === focusId ? 'calc-node--focus' : undefined,
108
+ data: { ...node.data, ...data },
109
+ }));
110
+ const edges = positioned.edges.map((edge) => {
111
+ const points = routes?.get(edge.id);
112
+ return points ? { ...edge, data: { ...edge.data, points } } : edge;
113
+ });
114
+ return { nodes, edges };
115
+ }, [placed, positioned.edges, data, focusId, routes]);
116
+ const { computed, extraRects } = useFlowWindow({
117
+ mergedFlow,
118
+ focusId,
119
+ draggedIds,
120
+ });
121
+ const {
122
+ nodes,
123
+ edges,
124
+ setState,
125
+ clearMounts,
126
+ } = useMountedNodes(computed);
127
+ const applyEngaged = useEdgeEngage({
128
+ computed,
129
+ setState,
130
+ rawFlow,
131
+ focusIdRef,
132
+ });
133
+ const updateNodeInternals = useUpdateNodeInternals();
134
+ const heightsRef = useRef<ReadonlyMap<string, number>>(new Map());
135
+ useEffect(() => {
136
+ heightsRef.current = new Map();
137
+ movedLiveRef.current = null;
138
+ dragIdsRef.current.clear();
139
+ pendingChangesRef.current = [];
140
+ }, [canvasId]);
141
+ useEffect(() => {
142
+ const prev = heightsRef.current;
143
+ const next = new Map<string, number>();
144
+ const changed: string[] = [];
145
+ for (const node of nodes) {
146
+ const h = node.height ?? 0;
147
+ next.set(node.id, h);
148
+ if (prev.has(node.id) && prev.get(node.id) !== h) changed.push(node.id);
149
+ }
150
+ heightsRef.current = next;
151
+ if (changed.length) updateNodeInternals(changed);
152
+ }, [nodes, updateNodeInternals]);
153
+
154
+ const [prevLayout, setPrevLayout] = useState(mode);
155
+ if (prevLayout !== mode) {
156
+ setPrevLayout(mode);
157
+ setMovedNodes(null);
158
+ setDraggedIds(new Set());
159
+ }
160
+ useEffect(() => {
161
+ movedLiveRef.current = null;
162
+ dragIdsRef.current.clear();
163
+ pendingChangesRef.current = [];
164
+ }, [mode, canvasId]);
165
+
166
+ const flushChanges = useCallback(() => {
167
+ changesFrameRef.current = null;
168
+ const batch = pendingChangesRef.current;
169
+ pendingChangesRef.current = [];
170
+ if (!batch.length) return;
171
+ setState((prev) => ({
172
+ ...prev,
173
+ nodes: applyNodeChanges<AnyCalcFlowNode>(batch, prev.nodes),
174
+ }));
175
+ const live = movedLiveRef.current;
176
+ const dragEnds: { id: string; x: number; y: number }[] = [];
177
+ for (const change of batch) {
178
+ if (change.type !== 'position' || change.dragging) continue;
179
+ if (!change.position || !dragIdsRef.current.delete(change.id)) continue;
180
+ dragEnds.push({ id: change.id, ...change.position });
181
+ data.onMoveCard(change.id, change.position);
182
+ }
183
+ setLiveMoved(live?.size ? new Map(live) : null);
184
+ if (!dragEnds.length) return;
185
+ setDraggedIds(new Set(dragIdsRef.current));
186
+ movedLiveRef.current = null;
187
+ setMovedNodes((prev) => {
188
+ const next = new Map(prev);
189
+ if (live) for (const [pid, pos] of live) next.set(pid, pos);
190
+ for (const end of dragEnds) next.set(end.id, { x: end.x, y: end.y });
191
+ return next;
192
+ });
193
+ }, [setState, data]);
194
+ useEffect(
195
+ () => () => {
196
+ if (changesFrameRef.current != null)
197
+ cancelAnimationFrame(changesFrameRef.current);
198
+ },
199
+ [],
200
+ );
201
+ const onNodesChange = useCallback(
202
+ (changes: NodeChange<AnyCalcFlowNode>[]) => {
203
+ for (const change of changes) {
204
+ if (change.type !== 'position' || !change.position) continue;
205
+ if (change.dragging) {
206
+ if (!dragIdsRef.current.has(change.id)) {
207
+ dragIdsRef.current.add(change.id);
208
+ setDraggedIds(new Set(dragIdsRef.current));
209
+ }
210
+ (movedLiveRef.current ??= new Map()).set(
211
+ change.id,
212
+ change.position,
213
+ );
214
+ }
215
+ }
216
+ pendingChangesRef.current.push(...changes);
217
+ if (changesFrameRef.current == null) {
218
+ changesFrameRef.current = requestAnimationFrame(flushChanges);
219
+ }
220
+ },
221
+ [flushChanges],
222
+ );
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
+
234
+ const clearMoved = useCallback(() => {
235
+ movedLiveRef.current = null;
236
+ setMovedNodes(null);
237
+ }, []);
238
+
239
+ const resetNodes = useCallback(() => {
240
+ setMovedNodes(null);
241
+ clearMounts();
242
+ setDraggedIds(new Set());
243
+ }, [clearMounts]);
244
+
245
+ return {
246
+ nodes,
247
+ edges,
248
+ mergedFlow,
249
+ extraRects,
250
+ movedNodes,
251
+ movedLiveRef,
252
+ clearMoved,
253
+ setMovedNodes,
254
+ onNodesChange,
255
+ onEdgesChange,
256
+ applyEngaged,
257
+ resetNodes,
258
+ };
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
+
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ type RefObject,
10
+ } from 'react';
11
+ import { useReactFlow } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
12
+ import type {
13
+ AnyCalcFlowNode,
14
+ CalcFlowNode,
15
+ CalcViewMode,
16
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
17
+
18
+ export function useFlowViewport({
19
+ mergedNodes,
20
+ dataset,
21
+ resetGen,
22
+ treeReady,
23
+ mode,
24
+ applyFocus,
25
+ focusSeqRef,
26
+ focusRef,
27
+ focusNodeId,
28
+ onFocusedNode,
29
+ focusId,
30
+ }: {
31
+ mergedNodes: AnyCalcFlowNode[];
32
+ dataset: DatasetRef;
33
+ resetGen: number;
34
+ treeReady: boolean;
35
+ mode: CalcViewMode;
36
+ applyFocus: (id: string | null) => void;
37
+ focusSeqRef: RefObject<{ id: string; index: number } | null>;
38
+ focusRef?: RefObject<((materialId: string) => void) | null>;
39
+ focusNodeId?: string | null;
40
+ onFocusedNode?: (id: string) => void;
41
+ focusId: string | null;
42
+ }) {
43
+ const { setCenter, getZoom } = useReactFlow();
44
+ const userMovedRef = useRef(false);
45
+ const storageKey = `calc:${dataset.id}/${dataset.version ?? ''}`;
46
+
47
+ const ready = treeReady;
48
+
49
+ const centerOnNode = useCallback(
50
+ (node: AnyCalcFlowNode) => {
51
+ void setCenter(node.position.x, node.position.y, {
52
+ zoom: Math.max(getZoom(), 1),
53
+ duration: 300,
54
+ });
55
+ },
56
+ [setCenter, getZoom],
57
+ );
58
+
59
+ useEffect(() => {
60
+ const ref = focusRef;
61
+ if (!ref) return;
62
+
63
+ ref.current = (materialId: string) => {
64
+ const wanted = materialId.replace(/\{.*$/, '');
65
+ const matches = mergedNodes.filter(
66
+ (entry) =>
67
+ (entry.data as CalcFlowNode['data']).node?.materialId?.replace(
68
+ /\{.*$/,
69
+ '',
70
+ ) === wanted,
71
+ );
72
+ if (!matches.length) return;
73
+ const seq = focusSeqRef.current;
74
+ const index =
75
+ seq?.id === materialId ? (seq.index + 1) % matches.length : 0;
76
+ focusSeqRef.current = { id: materialId, index };
77
+ const node = matches[index]! as AnyCalcFlowNode;
78
+ applyFocus(node.id);
79
+ centerOnNode(node);
80
+ };
81
+ return () => {
82
+ ref.current = null;
83
+ };
84
+ }, [focusRef, mergedNodes, applyFocus, centerOnNode, focusSeqRef]);
85
+
86
+ useEffect(() => {
87
+ if (!focusNodeId) return;
88
+ const node = mergedNodes.find((entry) => entry.id === focusNodeId);
89
+ if (!node) return;
90
+ applyFocus(node.id);
91
+ centerOnNode(node);
92
+ onFocusedNode?.(focusNodeId);
93
+ }, [mergedNodes, focusNodeId, applyFocus, centerOnNode, onFocusedNode]);
94
+
95
+ const recenterRef = useRef(false);
96
+ const viewSigRef = useRef(mode);
97
+ useEffect(() => {
98
+ if (viewSigRef.current === mode) return;
99
+ viewSigRef.current = mode;
100
+ recenterRef.current = true;
101
+ }, [mode]);
102
+ useEffect(() => {
103
+ if (!ready || !recenterRef.current) return;
104
+ const root = mergedNodes[0];
105
+ if (root) centerOnNode(root as AnyCalcFlowNode);
106
+ recenterRef.current = false;
107
+ }, [ready, mergedNodes, centerOnNode]);
108
+
109
+ const handleMoveStart = useCallback((event: unknown) => {
110
+ if (event) userMovedRef.current = true;
111
+ }, []);
112
+
113
+ const viewportConfig = useMemo(
114
+ () => ({
115
+ storageKey,
116
+ resetKey: `${storageKey}:${resetGen}`,
117
+ selectedId: focusId,
118
+ userMovedRef,
119
+ initFocusId: null,
120
+ focusZoom: 1,
121
+ minZoom: 0.02,
122
+ restoreViewport: false,
123
+ ready,
124
+ }),
125
+ [storageKey, resetGen, focusId, ready],
126
+ );
127
+ const shellProps = useMemo(
128
+ () => ({ 'data-ready': ready ? ('true' as const) : ('false' as const) }),
129
+ [ready],
130
+ );
131
+
132
+ return {
133
+ ready,
134
+ centerOnNode,
135
+ userMovedRef,
136
+ handleMoveStart,
137
+ viewportConfig,
138
+ shellProps,
139
+ };
140
+ }
141
+