@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,87 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useStore, useStoreApi } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
5
+ import { flowContentBox } from '@terrafirmagreg-team/ui/lib/canvas/geometry';
6
+ import {
7
+ CALC_NODE_H,
8
+ CALC_NODE_W,
9
+ type AnyCalcFlowNode,
10
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
11
+ import type { Edge } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
12
+
13
+ const CALC_VIRTUAL_MIN = 350;
14
+
15
+ export function useFlowWindow({
16
+ mergedFlow,
17
+ focusId,
18
+ draggedIds,
19
+ }: {
20
+ mergedFlow: { nodes: AnyCalcFlowNode[]; edges: Edge[] };
21
+ focusId: string | null;
22
+ draggedIds: ReadonlySet<string>;
23
+ }) {
24
+ const storeApi = useStoreApi();
25
+ const winKey = useStore((s) => {
26
+ const [tx, ty, z] = s.transform;
27
+ if (!s.width || !s.height || !z) return 'cold';
28
+ const w = s.width / z;
29
+ const h = s.height / z;
30
+ return `${Math.floor(-tx / z / w)}:${Math.floor(-ty / z / h)}:${Math.round(z * 4)}`;
31
+ });
32
+ const fullBox = useMemo(
33
+ () => flowContentBox(mergedFlow.nodes),
34
+ [mergedFlow.nodes],
35
+ );
36
+
37
+ const extraRects = useMemo(
38
+ () =>
39
+ fullBox
40
+ ? [
41
+ {
42
+ x: fullBox.midX,
43
+ y: fullBox.midY,
44
+ width: fullBox.width,
45
+ height: fullBox.height,
46
+ },
47
+ ]
48
+ : undefined,
49
+ [fullBox],
50
+ );
51
+ const computed = useMemo(() => {
52
+ if (mergedFlow.nodes.length <= CALC_VIRTUAL_MIN || winKey === 'cold') {
53
+ return mergedFlow;
54
+ }
55
+ const s = storeApi.getState();
56
+ const [tx, ty, z] = s.transform;
57
+ if (!s.width || !s.height || !z) return mergedFlow;
58
+ const vw = s.width / z;
59
+ const vh = s.height / z;
60
+ const left = -tx / z - vw;
61
+ const right = -tx / z + 2 * vw;
62
+ const top = -ty / z - vh;
63
+ const bottom = -ty / z + 2 * vh;
64
+ const nodes = mergedFlow.nodes.filter((node) => {
65
+ if (draggedIds.has(node.id) || node.id === focusId) return true;
66
+ const halfW = (node.width ?? CALC_NODE_W) / 2;
67
+ const halfH = (node.height ?? CALC_NODE_H) / 2;
68
+ return (
69
+ node.position.x + halfW >= left &&
70
+ node.position.x - halfW <= right &&
71
+ node.position.y + halfH >= top &&
72
+ node.position.y - halfH <= bottom
73
+ );
74
+ });
75
+ if (nodes.length === mergedFlow.nodes.length) return mergedFlow;
76
+ const ids = new Set(nodes.map((node) => node.id));
77
+ return {
78
+ nodes,
79
+ edges: mergedFlow.edges.filter(
80
+ (edge) => ids.has(edge.source) && ids.has(edge.target),
81
+ ),
82
+ };
83
+ }, [mergedFlow, winKey, focusId, draggedIds, storeApi]);
84
+
85
+ return { computed, extraRects };
86
+ }
87
+
@@ -0,0 +1,355 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useMemo,
6
+ type Dispatch,
7
+ type SetStateAction,
8
+ } from 'react';
9
+ import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
10
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
11
+ import { recipeCalcHref, recipeStackHref } from '@terrafirmagreg-team/recipe/lib/links';
12
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
13
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
14
+ import type {
15
+ CalcCard,
16
+ CalcKind,
17
+ CalcNode,
18
+ CalculatorState,
19
+ CalcWire,
20
+ } from '@terrafirmagreg-team/recipe/types/calc';
21
+ import type { CalcFlowData } from '@terrafirmagreg-team/recipe/lib/calc/flow';
22
+
23
+ let cardSeq = 0;
24
+ const newCardId = () =>
25
+ `c${Date.now().toString(36)}${(cardSeq += 1).toString(36)}`;
26
+
27
+ export function useCalcActions({
28
+ dataset,
29
+ lang,
30
+ cards,
31
+ wires,
32
+ tagSelections,
33
+ persist,
34
+ recipeDefaults,
35
+ setRecipeDefault,
36
+ recipeBody,
37
+ encodeShare,
38
+ flushAmounts,
39
+ setFocusNodeId,
40
+ nodes,
41
+ }: {
42
+ dataset: DatasetRef;
43
+ lang: Lang;
44
+ cards: CalcCard[];
45
+ wires: CalcWire[];
46
+ tagSelections: Record<string, string>;
47
+ persist: (next: CalculatorState) => void;
48
+ recipeDefaults: Record<string, string>;
49
+ setRecipeDefault: (materialId: string, recipeId: string | null) => void;
50
+ recipeBody: (recipeId: string) => Recipe | undefined;
51
+ encodeShare: () => string;
52
+ flushAmounts: () => CalcCard[];
53
+ setFocusNodeId: Dispatch<SetStateAction<string | null>>;
54
+ nodes: CalcNode[];
55
+ }) {
56
+ const { checked: shared, onClick: onShare } = useCopyButton(async () => {
57
+ const tree = encodeShare();
58
+ const { id: datasetId, version } = dataset;
59
+ try {
60
+ const res = (await fetch(
61
+ `/api/share/recipe/${encodeURIComponent(datasetId)}${version ? `?version=${encodeURIComponent(version)}` : ''}`,
62
+ {
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({ tree }),
66
+ },
67
+ ).then((r) => (r.ok ? r.json() : null))) as {
68
+ id?: string;
69
+ dataset?: DatasetRef;
70
+ } | null;
71
+ if (res?.id) {
72
+ await navigator.clipboard.writeText(
73
+ `${window.location.origin}${recipeCalcHref(res.dataset ?? dataset, undefined, `s${res.id}`)}`,
74
+ );
75
+ return;
76
+ }
77
+ } catch {
78
+ }
79
+ await navigator.clipboard.writeText(
80
+ `${window.location.origin}${recipeCalcHref(dataset, undefined, tree)}`,
81
+ );
82
+ });
83
+
84
+ const write = useCallback(
85
+ (next: Partial<Pick<CalculatorState, 'cards' | 'wires' | 'tagSelections'>>) => {
86
+ persist({
87
+ cards: next.cards ?? flushAmounts(),
88
+ wires: next.wires ?? wires,
89
+ tagSelections: next.tagSelections ?? tagSelections,
90
+ });
91
+ },
92
+ [flushAmounts, persist, wires, tagSelections],
93
+ );
94
+
95
+ const onSelectRecipe = useCallback(
96
+ (cardId: string, recipeId: string | null) => {
97
+ if (!cards.some((row) => row.id === cardId)) return;
98
+ const nextCards = cards.map((row) =>
99
+ row.id === cardId ? { ...row, recipeId } : row,
100
+ );
101
+ const nextWires = recipeId
102
+ ? wires
103
+ : wires.filter((wire) => wire.to !== cardId);
104
+ write({ cards: nextCards, wires: nextWires });
105
+ },
106
+ [cards, wires, write],
107
+ );
108
+
109
+ const onSelectTagMember = useCallback(
110
+ (tagId: string, memberId: string) => {
111
+ write({ tagSelections: { ...tagSelections, [tagId]: memberId } });
112
+ },
113
+ [tagSelections, write],
114
+ );
115
+
116
+ const onAddCard = useCallback(
117
+ (materialId: string, kind: CalcKind, pos?: { x: number; y: number }) => {
118
+ const id = newCardId();
119
+ persist({
120
+ cards: [
121
+ ...flushAmounts(),
122
+ { id, materialId, kind, recipeId: null, crafts: 1, pos },
123
+ ],
124
+ wires,
125
+ tagSelections,
126
+ });
127
+ setFocusNodeId(id);
128
+ return id;
129
+ },
130
+ [flushAmounts, persist, wires, tagSelections, setFocusNodeId],
131
+ );
132
+
133
+ const onSupplyInput = useCallback(
134
+ (cardId: string, inputIndex: number, recipeId: string | null, outputAmount?: number) => {
135
+ const consumerNode = nodes.find((n) => n.id === cardId);
136
+ const consumerCard = cards.find((row) => row.id === cardId);
137
+ const input = consumerNode?.inputs[inputIndex];
138
+ if (!consumerNode || !consumerCard || !input?.id) return null;
139
+ const need = Math.max(1, input.amount - input.covered);
140
+ const crafts = recipeId && outputAmount
141
+ ? Math.max(1, Math.ceil(need / outputAmount))
142
+ : need;
143
+ const base = consumerCard.pos;
144
+ const id = newCardId();
145
+ const nextWires = [
146
+ ...wires.filter(
147
+ (wire) => !(wire.to === cardId && wire.input === inputIndex),
148
+ ),
149
+ { from: id, to: cardId, input: inputIndex },
150
+ ];
151
+ persist({
152
+ cards: [
153
+ ...flushAmounts(),
154
+ {
155
+ id,
156
+ materialId: input.id,
157
+ kind: input.kind,
158
+ recipeId,
159
+ crafts,
160
+ pos: base
161
+ ? { x: base.x - 40, y: base.y + 140 + inputIndex * 24 }
162
+ : undefined,
163
+ },
164
+ ],
165
+ wires: nextWires,
166
+ tagSelections,
167
+ });
168
+ return id;
169
+ },
170
+ [nodes, cards, wires, flushAmounts, persist, tagSelections],
171
+ );
172
+
173
+ const onOpenItem = useCallback(
174
+ (materialId: string, kind: CalcKind) => {
175
+ window.open(
176
+ recipeStackHref(dataset, lang, materialId, { kind }),
177
+ '_blank',
178
+ 'noopener',
179
+ );
180
+ },
181
+ [dataset, lang],
182
+ );
183
+
184
+ const onRemoveCard = useCallback(
185
+ (cardId: string) => {
186
+ write({
187
+ cards: flushAmounts().filter((row) => row.id !== cardId),
188
+ wires: wires.filter(
189
+ (wire) => wire.from !== cardId && wire.to !== cardId,
190
+ ),
191
+ });
192
+ },
193
+ [flushAmounts, write, wires],
194
+ );
195
+
196
+ const onMoveCard = useCallback(
197
+ (cardId: string, pos: { x: number; y: number }) => {
198
+ write({
199
+ cards: flushAmounts().map((row) =>
200
+ row.id === cardId ? { ...row, pos } : row,
201
+ ),
202
+ });
203
+ },
204
+ [flushAmounts, write],
205
+ );
206
+
207
+ const onMoveCards = useCallback(
208
+ (positions: Map<string, { x: number; y: number }>) => {
209
+ write({
210
+ cards: flushAmounts().map((row) => {
211
+ const pos = positions.get(row.id);
212
+ return pos ? { ...row, pos } : row;
213
+ }),
214
+ });
215
+ },
216
+ [flushAmounts, write],
217
+ );
218
+
219
+ const onWire = useCallback(
220
+ (from: string, to: string, input: number, out?: number) => {
221
+ if (from === to) return;
222
+ write({
223
+ wires: [
224
+ ...wires.filter(
225
+ (wire) => !(wire.to === to && wire.input === input),
226
+ ),
227
+ { from, to, input, out: out || undefined },
228
+ ],
229
+ });
230
+ },
231
+ [wires, write],
232
+ );
233
+
234
+ const onUnwire = useCallback(
235
+ (from: string, to: string, input: number, out?: number) => {
236
+ write({
237
+ wires: wires.filter(
238
+ (wire) =>
239
+ !(
240
+ wire.from === from &&
241
+ wire.to === to &&
242
+ wire.input === input &&
243
+ (wire.out ?? 0) === (out ?? 0)
244
+ ),
245
+ ),
246
+ });
247
+ },
248
+ [wires, write],
249
+ );
250
+
251
+ const onRewire = useCallback(
252
+ (
253
+ from: string,
254
+ to: string,
255
+ input: number,
256
+ next: { from: string; to: string; input: number; out?: number },
257
+ ) => {
258
+ if (next.from === next.to) return;
259
+ write({
260
+ wires: [
261
+ ...wires.filter(
262
+ (wire) =>
263
+ !(wire.from === from && wire.to === to && wire.input === input) &&
264
+ !(wire.to === next.to && wire.input === next.input),
265
+ ),
266
+ next,
267
+ ],
268
+ });
269
+ },
270
+ [wires, write],
271
+ );
272
+
273
+ const onWireAmount = useCallback(
274
+ (from: string, to: string, input: number, amount: number | null) => {
275
+ write({
276
+ wires: wires.map((wire) =>
277
+ wire.from === from && wire.to === to && wire.input === input
278
+ ? { ...wire, amount: amount ?? undefined }
279
+ : wire,
280
+ ),
281
+ });
282
+ },
283
+ [wires, write],
284
+ );
285
+
286
+ const onClear = useCallback(() => {
287
+ persist({
288
+ cards: [],
289
+ wires: [],
290
+ tagSelections: {},
291
+ });
292
+ }, [persist]);
293
+
294
+ const flowData = useMemo<
295
+ Omit<
296
+ CalcFlowData,
297
+ | 'node'
298
+ | 'mode'
299
+ | 'direction'
300
+ | 'onRequestPicker'
301
+ | 'onRequestMemberPicker'
302
+ | 'onRequestSupply'
303
+ | 'onCrafts'
304
+ | 'getRecipesForItem'
305
+ | 'ensureMaterial'
306
+ >
307
+ >(
308
+ () => ({
309
+ dataset,
310
+ recipeBody,
311
+ onSelectRecipe,
312
+ onSelectTagMember,
313
+ onSupplyInput,
314
+ onAddCard,
315
+ onOpenItem,
316
+ onRemoveCard,
317
+ onMoveCard,
318
+ onWire,
319
+ onUnwire,
320
+ onRewire,
321
+ onWireAmount,
322
+ recipeDefaults,
323
+ onSetRecipeDefault: setRecipeDefault,
324
+ }),
325
+ [
326
+ dataset,
327
+ recipeBody,
328
+ onSelectRecipe,
329
+ onSelectTagMember,
330
+ onSupplyInput,
331
+ onAddCard,
332
+ onOpenItem,
333
+ onRemoveCard,
334
+ onMoveCard,
335
+ onWire,
336
+ onUnwire,
337
+ onRewire,
338
+ onWireAmount,
339
+ recipeDefaults,
340
+ setRecipeDefault,
341
+ ],
342
+ );
343
+
344
+ return {
345
+ shared,
346
+ onShare,
347
+ flowData,
348
+ onAddCard,
349
+ onRemoveCard,
350
+ onMoveCards,
351
+ onSupplyInput,
352
+ onClear,
353
+ };
354
+ }
355
+
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import type {
5
+ CalcCard,
6
+ CalcWire,
7
+ CalculatorState,
8
+ } from '@terrafirmagreg-team/recipe/types/calc';
9
+
10
+ export function useCalcAmounts({
11
+ cards,
12
+ wires,
13
+ tagSelections,
14
+ persist,
15
+ }: {
16
+ cards: CalcCard[];
17
+ wires: CalcWire[];
18
+ tagSelections: Record<string, string>;
19
+ persist: (next: CalculatorState) => void;
20
+ }) {
21
+ const [pending, setPending] = useState<CalcCard[] | null>(null);
22
+ const amountTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
23
+
24
+ useEffect(
25
+ () => () => {
26
+ if (amountTimer.current) clearTimeout(amountTimer.current);
27
+ },
28
+ [],
29
+ );
30
+
31
+ const flushAmounts = useCallback((): CalcCard[] => {
32
+ if (amountTimer.current) {
33
+ clearTimeout(amountTimer.current);
34
+ amountTimer.current = undefined;
35
+ }
36
+ if (!pending) return cards;
37
+ setPending(null);
38
+ return pending;
39
+ }, [pending, cards]);
40
+
41
+ const onCrafts = useCallback(
42
+ (cardId: string, crafts: number) => {
43
+ const base = pending ?? cards;
44
+ const next = base.map((card) =>
45
+ card.id === cardId ? { ...card, crafts: Math.max(1, crafts) } : card,
46
+ );
47
+ setPending(next);
48
+ if (amountTimer.current) clearTimeout(amountTimer.current);
49
+ amountTimer.current = setTimeout(() => {
50
+ amountTimer.current = undefined;
51
+ setPending(null);
52
+ persist({ cards: next, wires, tagSelections });
53
+ }, 300);
54
+ },
55
+ [pending, cards, persist, wires, tagSelections],
56
+ );
57
+
58
+ return {
59
+ displayCards: pending ?? cards,
60
+ flushAmounts,
61
+ onCrafts,
62
+ };
63
+ }
64
+
@@ -0,0 +1,203 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+ import { encodeCalcState } from '@terrafirmagreg-team/recipe/lib/calc/tree';
5
+ import {
6
+ summaryCacheFor,
7
+ summarizeRecipes,
8
+ } from '@terrafirmagreg-team/recipe/lib/calc/resolve';
9
+ import { buildScheme, schemeSummary } from '@terrafirmagreg-team/recipe/lib/calc/scheme';
10
+ import { calcStorageKey } from '@terrafirmagreg-team/recipe/lib/calc/storage';
11
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
12
+ import type {
13
+ CalcKind,
14
+ CalcRecipeSummary,
15
+ } from '@terrafirmagreg-team/recipe/types/calc';
16
+ import { useCalcStorage } from './use-storage';
17
+ import { useCalcShare } from './use-share';
18
+ import { useCalcMaps } from './use-maps';
19
+ import { useCalcWorker } from './use-worker';
20
+ import { useCalcResolver } from './use-resolver';
21
+ import { useRecipeDefaults } from './use-defaults';
22
+
23
+ export function useCalcData(session: RecipeSession) {
24
+ const { dataset, lang } = session;
25
+ const catalog = session.index;
26
+ const storageKey = calcStorageKey('recipe', dataset);
27
+ const sharePending = useCalcShare({
28
+ storageKey,
29
+ dataset,
30
+ lang,
31
+ incoming: session.tree ?? '',
32
+ });
33
+ const {
34
+ calcState,
35
+ tabs,
36
+ activeTabId,
37
+ history,
38
+ persist,
39
+ openTab,
40
+ openEntry,
41
+ closeTab,
42
+ newTab,
43
+ renameTab,
44
+ setTabIcon,
45
+ removeHistory,
46
+ } = useCalcStorage(storageKey);
47
+ const { defaults, setRecipeDefault } = useRecipeDefaults(dataset);
48
+ const { maps, data, applyMaps, resetMaps } = useCalcMaps();
49
+ const { bodyMap } = maps;
50
+ const {
51
+ outMap: dataOutMap,
52
+ recipeMap: dataRecipeMap,
53
+ tagMap: dataTagMap,
54
+ deadIds: dataDeadIds,
55
+ } = data;
56
+
57
+ const cards = useMemo(() => calcState?.cards ?? [], [calcState]);
58
+ const wires = useMemo(() => calcState?.wires ?? [], [calcState]);
59
+ const tagSelections = useMemo(
60
+ () => calcState?.tagSelections ?? {},
61
+ [calcState],
62
+ );
63
+
64
+ const getRecipesForItem = useCallback(
65
+ (materialId: string, kind: CalcKind): CalcRecipeSummary[] => {
66
+ if (!catalog) return [];
67
+ return summarizeRecipes(
68
+ dataOutMap,
69
+ dataRecipeMap,
70
+ catalog.types,
71
+ summaryCacheFor(dataOutMap, dataRecipeMap),
72
+ materialId,
73
+ kind,
74
+ );
75
+ },
76
+ [catalog, dataOutMap, dataRecipeMap],
77
+ );
78
+
79
+ const tagMembers = useCallback(
80
+ (tagId: string) => dataTagMap.get(tagId),
81
+ [dataTagMap],
82
+ );
83
+
84
+ const { nodes, flows } = useMemo(
85
+ () =>
86
+ buildScheme({
87
+ cards,
88
+ wires,
89
+ ctx: {
90
+ recipeMap: dataRecipeMap,
91
+ getRecipesForItem,
92
+ tagMembers,
93
+ tagSelections,
94
+ recipesResolved: (id) => dataOutMap.has(id),
95
+ deadIds: dataDeadIds,
96
+ },
97
+ }),
98
+ [
99
+ cards,
100
+ wires,
101
+ dataRecipeMap,
102
+ getRecipesForItem,
103
+ tagMembers,
104
+ tagSelections,
105
+ dataOutMap,
106
+ dataDeadIds,
107
+ ],
108
+ );
109
+
110
+ const summary = useMemo(() => schemeSummary(nodes), [nodes]);
111
+
112
+ const inputKey = useMemo(
113
+ () => JSON.stringify([cards, wires, tagSelections]),
114
+ [cards, wires, tagSelections],
115
+ );
116
+
117
+ const worker = useCalcWorker({
118
+ dataset,
119
+ catalog,
120
+ activeTabId,
121
+ applyMaps,
122
+ resetMaps,
123
+ });
124
+
125
+ useCalcResolver({
126
+ catalog,
127
+ inputKey,
128
+ cards,
129
+ recipeMap: dataRecipeMap,
130
+ worker,
131
+ });
132
+
133
+ const dataSettled = useMemo(() => {
134
+ for (const card of cards) {
135
+ if (!dataOutMap.has(card.materialId)) return false;
136
+ if (
137
+ card.recipeId &&
138
+ !dataRecipeMap.has(card.recipeId) &&
139
+ !dataDeadIds.has(card.recipeId)
140
+ ) {
141
+ return false;
142
+ }
143
+ }
144
+ return true;
145
+ }, [cards, dataOutMap, dataRecipeMap, dataDeadIds]);
146
+ const settled = dataSettled && !sharePending;
147
+
148
+ const encodeShare = useCallback(
149
+ () =>
150
+ encodeCalcState({
151
+ cards,
152
+ wires,
153
+ tagSelections,
154
+ }),
155
+ [cards, wires, tagSelections],
156
+ );
157
+
158
+ const recipeBody = useCallback(
159
+ (recipeId: string) => bodyMap.get(recipeId),
160
+ [bodyMap],
161
+ );
162
+
163
+ const ensureMaterial = useCallback(
164
+ (materialId: string) => {
165
+ if (dataOutMap.has(materialId)) return;
166
+ void worker
167
+ .call((api) => api.resolve({ materials: [materialId] }))
168
+ .catch(() => {});
169
+ },
170
+ [dataOutMap, worker],
171
+ );
172
+
173
+ return {
174
+ cards,
175
+ wires,
176
+ tagSelections,
177
+ tabs,
178
+ activeTabId,
179
+ history,
180
+ openTab,
181
+ openEntry,
182
+ closeTab,
183
+ newTab,
184
+ renameTab,
185
+ setTabIcon,
186
+ removeHistory,
187
+ recipeDefaults: defaults,
188
+ setRecipeDefault,
189
+ getRecipesForItem,
190
+ ensureMaterial,
191
+ tagMembers,
192
+ nodes,
193
+ flows,
194
+ summary,
195
+ persist,
196
+ encodeShare,
197
+ recipeBody,
198
+ settled,
199
+ bodiesLoaded: maps.bodyMap.size,
200
+ workerSettled: worker.settled,
201
+ };
202
+ }
203
+