@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,394 @@
1
+ import PF from 'pathfinding';
2
+ import type { Edge } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
3
+ import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
4
+ import { createRectBuckets } from './buckets';
5
+ import {
6
+ CELL,
7
+ PAD,
8
+ cellKey,
9
+ keyX,
10
+ keyY,
11
+ portAnchor,
12
+ toRect,
13
+ zRoute,
14
+ type Rect,
15
+ type RouteNode,
16
+ } from './geometry';
17
+ import { cellsToPolyline, orthoPoints, simplifyCells } from './polyline';
18
+
19
+ const WINDOW = 160;
20
+
21
+ const MAX_CELLS = 200_000;
22
+
23
+ const STUB = 40;
24
+
25
+ function stubLength(
26
+ from: FlowPoint,
27
+ dir: 1 | -1,
28
+ skip: Rect[],
29
+ clear: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
30
+ ): number {
31
+ let max = STUB;
32
+ const end = from.x + dir * STUB;
33
+ const x0 = Math.min(from.x, end) - 1;
34
+ const x1 = Math.max(from.x, end) + 1;
35
+ for (const rect of clear(x0, from.y - 1, x1, from.y + 1)) {
36
+ if (skip.includes(rect)) continue;
37
+ if (from.y < rect.y - PAD || from.y > rect.y + rect.h + PAD) continue;
38
+ const edge = dir === 1 ? rect.x - PAD : rect.x + rect.w + PAD;
39
+ const room = (edge - from.x) * dir;
40
+ if (room < max) max = Math.max(0, room);
41
+ }
42
+ return max;
43
+ }
44
+
45
+ function growWindow(
46
+ bounds: { x0: number; y0: number; x1: number; y1: number },
47
+ query: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
48
+ pad: number,
49
+ ): void {
50
+ for (let i = 0; i < 4; i += 1) {
51
+ let grew = false;
52
+ for (const rect of query(bounds.x0, bounds.y0, bounds.x1, bounds.y1)) {
53
+ const rx0 = rect.x - pad;
54
+ const ry0 = rect.y - pad;
55
+ const rx1 = rect.x + rect.w + pad;
56
+ const ry1 = rect.y + rect.h + pad;
57
+ if (rx0 < bounds.x0) { bounds.x0 = rx0; grew = true; }
58
+ if (ry0 < bounds.y0) { bounds.y0 = ry0; grew = true; }
59
+ if (rx1 > bounds.x1) { bounds.x1 = rx1; grew = true; }
60
+ if (ry1 > bounds.y1) { bounds.y1 = ry1; grew = true; }
61
+ }
62
+ if (!grew) break;
63
+ }
64
+ }
65
+
66
+ function detourRoute(
67
+ a: FlowPoint,
68
+ b: FlowPoint,
69
+ sa: FlowPoint,
70
+ sb: FlowPoint,
71
+ skip: Rect[],
72
+ query: (x0: number, y0: number, x1: number, y1: number) => Iterable<Rect>,
73
+ ): FlowPoint[] {
74
+ const bounds = {
75
+ x0: Math.min(sa.x, sb.x),
76
+ y0: Math.min(sa.y, sb.y),
77
+ x1: Math.max(sa.x, sb.x),
78
+ y1: Math.max(sa.y, sb.y),
79
+ };
80
+ growWindow(bounds, query, PAD);
81
+ const laneAt = (off: number): FlowPoint[][] => [
82
+ [sa, { x: bounds.x1 + off, y: sa.y }, { x: bounds.x1 + off, y: sb.y }, sb],
83
+ [sa, { x: bounds.x0 - off, y: sa.y }, { x: bounds.x0 - off, y: sb.y }, sb],
84
+ [sa, { x: sa.x, y: bounds.y0 - off }, { x: sb.x, y: bounds.y0 - off }, sb],
85
+ [sa, { x: sa.x, y: bounds.y1 + off }, { x: sb.x, y: bounds.y1 + off }, sb],
86
+ ];
87
+ const clearLane = (lane: FlowPoint[]): number | null => {
88
+ let len = 0;
89
+ for (let i = 0; i + 1 < lane.length; i += 1) {
90
+ const p = lane[i]!;
91
+ const q = lane[i + 1]!;
92
+ len += Math.abs(q.x - p.x) + Math.abs(q.y - p.y);
93
+ const sx0 = Math.min(p.x, q.x) - 1;
94
+ const sy0 = Math.min(p.y, q.y) - 1;
95
+ const sx1 = Math.max(p.x, q.x) + 1;
96
+ const sy1 = Math.max(p.y, q.y) + 1;
97
+ for (const rect of query(sx0, sy0, sx1, sy1)) {
98
+ if (skip.includes(rect)) continue;
99
+ return null;
100
+ }
101
+ }
102
+ return len;
103
+ };
104
+ for (let ring = 0; ring < 6; ring += 1) {
105
+ const off = CELL * (ring === 0 ? 1 : 2 ** (ring + 1));
106
+ let best: FlowPoint[] | null = null;
107
+ let bestLen = Infinity;
108
+ for (const lane of laneAt(off)) {
109
+ const len = clearLane(lane);
110
+ if (len !== null && len < bestLen) {
111
+ bestLen = len;
112
+ best = lane;
113
+ }
114
+ }
115
+ if (best) return best;
116
+ growWindow(bounds, query, PAD);
117
+ }
118
+ return zRoute(a, b, sa, sb).slice(1, -1);
119
+ }
120
+
121
+ type Committed = { keys: number[]; x0: number; y0: number; x1: number; y1: number };
122
+
123
+ type RouteEdge = Pick<Edge, 'id' | 'source' | 'target'> & {
124
+ sourceHandle?: string | null;
125
+ targetHandle?: string | null;
126
+ };
127
+
128
+ type EdgeState = {
129
+ points: FlowPoint[];
130
+ a: FlowPoint;
131
+ b: FlowPoint;
132
+ committed?: Committed;
133
+ };
134
+
135
+ const samePoint = (a: FlowPoint, b: FlowPoint) => a.x === b.x && a.y === b.y;
136
+
137
+ const targetInput = (edge: RouteEdge) => {
138
+ const handle = edge.targetHandle;
139
+ const n = handle?.startsWith('in-') ? Number(handle.slice(3)) : 0;
140
+ return Number.isFinite(n) ? n : 0;
141
+ };
142
+
143
+ const sourceOut = (edge: RouteEdge) => {
144
+ const n = /^out-(\d+)$/.exec(edge.sourceHandle ?? '')?.[1];
145
+ return n != null && Number.isFinite(Number(n)) ? Number(n) : 0;
146
+ };
147
+
148
+ export function createCircuitRouter(): (
149
+ nodes: RouteNode[],
150
+ edges: RouteEdge[],
151
+ ) => Map<string, FlowPoint[]> {
152
+ let prevEdges = new Map<string, EdgeState>();
153
+
154
+ return (nodes, edges) => {
155
+ const out = new Map<string, FlowPoint[]>();
156
+ const nextEdges = new Map<string, EdgeState>();
157
+ if (!edges.length) {
158
+ prevEdges = nextEdges;
159
+ return out;
160
+ }
161
+
162
+ const byId = new Map(nodes.map((node) => [node.id, node]));
163
+ const rects = new Map(nodes.map((node) => [node.id, toRect(node)]));
164
+ const rectsInWindow = createRectBuckets(rects.values());
165
+
166
+ const committed: Committed[] = [];
167
+ const used = new Set<number>();
168
+ const pending: {
169
+ edge: RouteEdge;
170
+ s: RouteNode;
171
+ t: RouteNode;
172
+ a: FlowPoint;
173
+ b: FlowPoint;
174
+ dist: number;
175
+ }[] = [];
176
+
177
+ for (const edge of edges) {
178
+ const s = byId.get(edge.source);
179
+ const t = byId.get(edge.target);
180
+ if (!s || !t) continue;
181
+ const a = portAnchor(s, 'src', sourceOut(edge));
182
+ const b = portAnchor(t, 'tgt', targetInput(edge));
183
+ const prev = prevEdges.get(edge.id);
184
+ if (prev && samePoint(prev.a, a) && samePoint(prev.b, b)) {
185
+ out.set(edge.id, prev.points);
186
+ nextEdges.set(edge.id, prev);
187
+ if (prev.committed) {
188
+ committed.push(prev.committed);
189
+ for (const key of prev.committed.keys) used.add(key);
190
+ }
191
+ continue;
192
+ }
193
+ pending.push({
194
+ edge,
195
+ s,
196
+ t,
197
+ a,
198
+ b,
199
+ dist: Math.abs(a.x - b.x) + Math.abs(a.y - b.y),
200
+ });
201
+ }
202
+
203
+ pending.sort((x, y) => x.dist - y.dist || x.edge.id.localeCompare(y.edge.id));
204
+
205
+ const finder = new PF.AStarFinder({
206
+ diagonalMovement: PF.DiagonalMovement.Never,
207
+ });
208
+
209
+ for (const job of pending) {
210
+ const { a, b } = job;
211
+
212
+ const gap = b.x - a.x;
213
+ if (gap > 0 && a.y === b.y) {
214
+ const points = [a, b];
215
+ out.set(job.edge.id, points);
216
+ nextEdges.set(job.edge.id, { points, a, b });
217
+ continue;
218
+ }
219
+
220
+ const sa = {
221
+ x: a.x + stubLength(a, 1, [rects.get(job.s.id)!], rectsInWindow),
222
+ y: a.y,
223
+ };
224
+ const sb = {
225
+ x: b.x - stubLength(b, -1, [rects.get(job.t.id)!], rectsInWindow),
226
+ y: b.y,
227
+ };
228
+ const bounds = {
229
+ x0: Math.min(sa.x, sb.x) - WINDOW,
230
+ y0: Math.min(sa.y, sb.y) - WINDOW,
231
+ x1: Math.max(sa.x, sb.x) + WINDOW,
232
+ y1: Math.max(sa.y, sb.y) + WINDOW,
233
+ };
234
+ growWindow(bounds, rectsInWindow, PAD + CELL);
235
+ const wx0 = bounds.x0;
236
+ const wy0 = bounds.y0;
237
+ const wx1 = bounds.x1;
238
+ const wy1 = bounds.y1;
239
+ const ox = Math.floor(wx0 / CELL);
240
+ const oy = Math.floor(wy0 / CELL);
241
+ const gw = Math.ceil(wx1 / CELL) - ox + 1;
242
+ const gh = Math.ceil(wy1 / CELL) - oy + 1;
243
+
244
+ let cells: number[][] | null = null;
245
+ let passGrid: InstanceType<typeof PF.Grid> | null = null;
246
+ if (gw > 0 && gh > 0 && gw * gh <= MAX_CELLS) {
247
+ const base = new PF.Grid(gw, gh);
248
+ for (const rect of rectsInWindow(wx0, wy0, wx1, wy1)) {
249
+ const rx0 = Math.max(0, Math.floor((rect.x - PAD) / CELL) - ox);
250
+ const ry0 = Math.max(0, Math.floor((rect.y - PAD) / CELL) - oy);
251
+ const rx1 = Math.min(
252
+ gw - 1,
253
+ Math.ceil((rect.x + rect.w + PAD) / CELL) - ox - 1,
254
+ );
255
+ const ry1 = Math.min(
256
+ gh - 1,
257
+ Math.ceil((rect.y + rect.h + PAD) / CELL) - oy - 1,
258
+ );
259
+ for (let cy = ry0; cy <= ry1; cy += 1) {
260
+ for (let cx = rx0; cx <= rx1; cx += 1) {
261
+ base.setWalkableAt(cx, cy, false);
262
+ }
263
+ }
264
+ }
265
+ const sCell = {
266
+ x: Math.min(gw - 1, Math.max(0, Math.round(sa.x / CELL) - ox)),
267
+ y: Math.min(gh - 1, Math.max(0, Math.round(sa.y / CELL) - oy)),
268
+ };
269
+ const tCell = {
270
+ x: Math.min(gw - 1, Math.max(0, Math.round(sb.x / CELL) - ox)),
271
+ y: Math.min(gh - 1, Math.max(0, Math.round(sb.y / CELL) - oy)),
272
+ };
273
+ let best: number[][] | null = null;
274
+ let bestGrid: InstanceType<typeof PF.Grid> | null = null;
275
+ for (let pass = 0; pass < 2; pass += 1) {
276
+ const grid = base.clone();
277
+ if (pass === 0) {
278
+ for (const c of committed) {
279
+ if (
280
+ c.x1 < ox ||
281
+ c.x0 > ox + gw - 1 ||
282
+ c.y1 < oy ||
283
+ c.y0 > oy + gh - 1
284
+ ) {
285
+ continue;
286
+ }
287
+ for (const key of c.keys) {
288
+ const cx = keyX(key) - ox;
289
+ const cy = keyY(key) - oy;
290
+ if (cx >= 0 && cx < gw && cy >= 0 && cy < gh) {
291
+ grid.setWalkableAt(cx, cy, false);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ grid.setWalkableAt(sCell.x, sCell.y, true);
297
+ grid.setWalkableAt(tCell.x, tCell.y, true);
298
+ const path = finder.findPath(
299
+ sCell.x,
300
+ sCell.y,
301
+ tCell.x,
302
+ tCell.y,
303
+ grid,
304
+ );
305
+ if (!path.length) continue;
306
+ const mapped = path.map(([cx, cy]) => [cx + ox, cy + oy]);
307
+ if (pass === 1) {
308
+ if (best && best.length <= mapped.length * 1.34) break;
309
+ best = mapped;
310
+ bestGrid = grid;
311
+ break;
312
+ }
313
+ best = mapped;
314
+ bestGrid = grid;
315
+ }
316
+ cells = best;
317
+ passGrid = bestGrid;
318
+ }
319
+
320
+ if (!cells || !passGrid) {
321
+ const points = orthoPoints([
322
+ a,
323
+ ...detourRoute(
324
+ a,
325
+ b,
326
+ sa,
327
+ sb,
328
+ [rects.get(job.s.id)!, rects.get(job.t.id)!],
329
+ rectsInWindow,
330
+ ),
331
+ b,
332
+ ]);
333
+ out.set(job.edge.id, points);
334
+ nextEdges.set(job.edge.id, { points, a, b });
335
+ continue;
336
+ }
337
+
338
+ const g = passGrid;
339
+ const simple = simplifyCells(
340
+ cells,
341
+ (x, y) =>
342
+ x - ox >= 0 &&
343
+ x - ox < gw &&
344
+ y - oy >= 0 &&
345
+ y - oy < gh &&
346
+ g.isWalkableAt(x - ox, y - oy),
347
+ );
348
+ const polyline = cellsToPolyline(simple, sa, sb);
349
+ const points = orthoPoints([a, sa, ...polyline, sb, b]);
350
+ out.set(job.edge.id, points);
351
+
352
+ const keys: number[] = [];
353
+ let cx0 = Infinity;
354
+ let cy0 = Infinity;
355
+ let cx1 = -Infinity;
356
+ let cy1 = -Infinity;
357
+ const mark = (cx: number, cy: number) => {
358
+ keys.push(cellKey(cx, cy));
359
+ cx0 = Math.min(cx0, cx);
360
+ cy0 = Math.min(cy0, cy);
361
+ cx1 = Math.max(cx1, cx);
362
+ cy1 = Math.max(cy1, cy);
363
+ };
364
+ for (let i = 0; i < simple.length - 1; i += 1) {
365
+ const [fx, fy] = simple[i]!;
366
+ const [tx, ty] = simple[i + 1]!;
367
+ const dx = Math.sign(tx - fx);
368
+ const dy = Math.sign(ty - fy);
369
+ let cx = fx;
370
+ let cy = fy;
371
+ mark(cx, cy);
372
+ while (cx !== tx || cy !== ty) {
373
+ cx += dx;
374
+ cy += dy;
375
+ mark(cx, cy);
376
+ }
377
+ }
378
+ for (const key of keys) used.add(key);
379
+ const entry: Committed = {
380
+ keys,
381
+ x0: cx0 === Infinity ? 0 : cx0,
382
+ y0: cy0 === Infinity ? 0 : cy0,
383
+ x1: cx1 === -Infinity ? 0 : cx1,
384
+ y1: cy1 === -Infinity ? 0 : cy1,
385
+ };
386
+ committed.push(entry);
387
+ nextEdges.set(job.edge.id, { points, a: job.a, b: job.b, committed: entry });
388
+ }
389
+
390
+ prevEdges = nextEdges;
391
+ return out;
392
+ };
393
+ }
394
+
@@ -0,0 +1,226 @@
1
+ import type {
2
+ CalcCard,
3
+ CalcKind,
4
+ CalcMaterial,
5
+ CalcNode,
6
+ CalcNodeInput,
7
+ CalcOutput,
8
+ CalcRecipe,
9
+ CalcRecipeSummary,
10
+ CalcWire,
11
+ TagResolution,
12
+ } from '@terrafirmagreg-team/recipe/types/calc';
13
+ import { mergeAmounts } from './tree/materials';
14
+
15
+ export type SchemeContext = {
16
+ recipeMap: Map<string, CalcRecipe>;
17
+ getRecipesForItem: (materialId: string, kind: CalcKind) => CalcRecipeSummary[];
18
+ tagMembers: (tagId: string) => TagResolution | undefined;
19
+ tagSelections: Record<string, string>;
20
+ recipesResolved: (id: string) => boolean;
21
+ deadIds: ReadonlySet<string>;
22
+ };
23
+
24
+ function outputAmount(
25
+ recipe: CalcRecipe,
26
+ materialId: string,
27
+ kind: CalcKind,
28
+ ): number {
29
+ return recipe.outputs
30
+ .filter((out) => out.id === materialId && out.kind === kind)
31
+ .reduce((sum, out) => sum + out.amount * (out.chance ?? 1), 0);
32
+ }
33
+
34
+ function inputMember(
35
+ input: CalcRecipe['inputs'][number],
36
+ ctx: SchemeContext,
37
+ ): { id?: string; kind: CalcKind } {
38
+ if (!input.tagId) return { id: input.id, kind: input.kind };
39
+ const picked = ctx.tagSelections[input.tagId];
40
+ const resolution = ctx.tagMembers(input.tagId);
41
+ const members = resolution?.members ?? [];
42
+ const memberId =
43
+ picked && members.includes(picked) ? picked : members[0];
44
+ return { id: memberId, kind: resolution?.kind ?? input.kind };
45
+ }
46
+
47
+ export function buildScheme({
48
+ cards,
49
+ wires,
50
+ ctx,
51
+ }: {
52
+ cards: readonly CalcCard[];
53
+ wires: readonly CalcWire[];
54
+ ctx: SchemeContext;
55
+ }): { nodes: CalcNode[]; flows: (number | undefined)[] } {
56
+ const byId = new Map<string, CalcNode>();
57
+ const nodes = cards.map((card) => {
58
+ const availableRecipes = ctx.getRecipesForItem(card.materialId, card.kind);
59
+ const recipe = card.recipeId
60
+ ? (ctx.recipeMap.get(card.recipeId) ?? null)
61
+ : null;
62
+ const resolved = card.recipeId ? !!recipe : true;
63
+ const out = recipe
64
+ ? outputAmount(recipe, card.materialId, card.kind)
65
+ : 1;
66
+ const produced = card.crafts * out;
67
+ const outputs: CalcOutput[] = [];
68
+ const addOutput = (entry: CalcOutput) => {
69
+ if (!(entry.amount > 0)) return;
70
+ const same = outputs.find(
71
+ (o) => o.id === entry.id && o.kind === entry.kind,
72
+ );
73
+ if (same) {
74
+ same.amount += entry.amount;
75
+ same.remaining += entry.amount;
76
+ } else {
77
+ outputs.push(entry);
78
+ }
79
+ };
80
+ addOutput({
81
+ id: card.materialId,
82
+ kind: card.kind,
83
+ amount: produced,
84
+ allocated: 0,
85
+ remaining: produced,
86
+ });
87
+ for (const o of recipe?.outputs ?? []) {
88
+ if (o.id === card.materialId && o.kind === card.kind) continue;
89
+ addOutput({
90
+ id: o.id,
91
+ kind: o.kind,
92
+ amount: o.amount * card.crafts,
93
+ chance: o.chance,
94
+ allocated: 0,
95
+ remaining: o.amount * card.crafts,
96
+ });
97
+ }
98
+ const node: CalcNode = {
99
+ id: card.id,
100
+ materialId: card.materialId,
101
+ kind: card.kind,
102
+ crafts: card.crafts,
103
+ produced,
104
+ allocated: 0,
105
+ remaining: produced,
106
+ outputs,
107
+ recipe,
108
+ inputs: (recipe?.inputs ?? []).map((entry) => {
109
+ const member = inputMember(entry, ctx);
110
+ return {
111
+ id: member.id,
112
+ kind: member.kind,
113
+ amount: entry.catalyst
114
+ ? entry.amount
115
+ : entry.amount * card.crafts,
116
+ catalyst: entry.catalyst,
117
+ tagId: entry.tagId,
118
+ wired: false,
119
+ covered: 0,
120
+ } satisfies CalcNodeInput;
121
+ }),
122
+ availableRecipes,
123
+ cycle: false,
124
+ products: false,
125
+ loading:
126
+ !ctx.recipesResolved(card.materialId) ||
127
+ (card.recipeId ? !resolved && !ctx.deadIds.has(card.recipeId) : false),
128
+ };
129
+ byId.set(card.id, node);
130
+ return node;
131
+ });
132
+
133
+ const flows: (number | undefined)[] = new Array(wires.length);
134
+ const deg = new Map<string, number>();
135
+ const adj = new Map<string, string[]>();
136
+ wires.forEach((wire, wireIndex) => {
137
+ const src = byId.get(wire.from);
138
+ const dst = byId.get(wire.to);
139
+ if (!src || !dst || src === dst) return;
140
+ const input = dst.inputs[wire.input];
141
+ const atIndex = src.outputs[wire.out ?? 0];
142
+ const outIndex =
143
+ input && atIndex && atIndex.id === input.id && atIndex.kind === input.kind
144
+ ? (wire.out ?? 0)
145
+ : input
146
+ ? src.outputs.findIndex(
147
+ (o) => o.id === input.id && o.kind === input.kind,
148
+ )
149
+ : -1;
150
+ const srcOut = outIndex >= 0 ? src.outputs[outIndex] : undefined;
151
+ if (!input || !srcOut) return;
152
+ if (!input.id) return;
153
+ input.wired = true;
154
+ const covered = Math.min(
155
+ Math.max(srcOut.remaining, 0),
156
+ input.amount - input.covered,
157
+ wire.amount ?? Infinity,
158
+ );
159
+ input.covered += covered;
160
+ srcOut.allocated += covered;
161
+ srcOut.remaining -= covered;
162
+ if (outIndex === 0) {
163
+ src.allocated += covered;
164
+ src.remaining -= covered;
165
+ }
166
+ flows[wireIndex] = covered;
167
+ adj.set(wire.from, [...(adj.get(wire.from) ?? []), wire.to]);
168
+ deg.set(wire.to, (deg.get(wire.to) ?? 0) + 1);
169
+ });
170
+
171
+ const cyclic = (start: string): boolean => {
172
+ const seen = new Set<string>();
173
+ const walk = (id: string): boolean => {
174
+ for (const next of adj.get(id) ?? []) {
175
+ if (next === start) return true;
176
+ if (seen.has(next)) continue;
177
+ seen.add(next);
178
+ if (walk(next)) return true;
179
+ }
180
+ return false;
181
+ };
182
+ return walk(start);
183
+ };
184
+ for (const node of nodes) node.cycle = cyclic(node.id);
185
+
186
+ for (const node of nodes) {
187
+ node.products = !(adj.get(node.id)?.length);
188
+ }
189
+ return { nodes, flows };
190
+ }
191
+
192
+ export function schemeSummary(nodes: readonly CalcNode[]) {
193
+ const raw: CalcMaterial[] = [];
194
+ const catalysts: CalcMaterial[] = [];
195
+ const surplus: CalcMaterial[] = [];
196
+ const products: CalcMaterial[] = [];
197
+ for (const node of nodes) {
198
+ for (const input of node.inputs) {
199
+ if (!input.id) continue;
200
+ if (input.catalyst) {
201
+ catalysts.push({ id: input.id, kind: input.kind, amount: input.amount });
202
+ continue;
203
+ }
204
+ const deficit = input.amount - input.covered;
205
+ if (deficit > 0) raw.push({ id: input.id, kind: input.kind, amount: deficit });
206
+ }
207
+ if (!node.recipe && node.produced > 0 && !node.products) {
208
+ raw.push({ id: node.materialId, kind: node.kind, amount: node.produced });
209
+ }
210
+ if (node.products && node.produced > 0) {
211
+ products.push({ id: node.materialId, kind: node.kind, amount: node.produced });
212
+ }
213
+ for (const out of node.outputs.slice(node.products ? 1 : 0)) {
214
+ if (out.remaining > 0) {
215
+ surplus.push({ id: out.id, kind: out.kind, amount: out.remaining });
216
+ }
217
+ }
218
+ }
219
+ return {
220
+ raw: mergeAmounts(raw),
221
+ catalysts,
222
+ remainders: mergeAmounts(surplus),
223
+ products: mergeAmounts(products),
224
+ };
225
+ }
226
+
@@ -0,0 +1,97 @@
1
+ import type { CalculatorState } from '@terrafirmagreg-team/recipe/types/calc';
2
+
3
+ const MAX_HISTORY = 20;
4
+
5
+ export type CalcTreeEntry = {
6
+ id: string;
7
+
8
+ name?: string;
9
+
10
+ icon?: { id: string; kind: 'item' | 'fluid' };
11
+ savedAt: number;
12
+ state: CalculatorState;
13
+ };
14
+
15
+ export const EMPTY_ENTRIES: CalcTreeEntry[] = [];
16
+
17
+ export function newCalcEntry(state: CalculatorState, name?: string): CalcTreeEntry {
18
+ return {
19
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
20
+ name,
21
+ savedAt: Date.now(),
22
+ state,
23
+ };
24
+ }
25
+
26
+ const isCardRow = (row: unknown): boolean =>
27
+ !!row &&
28
+ typeof row === 'object' &&
29
+ typeof (row as { id?: unknown }).id === 'string' &&
30
+ typeof (row as { materialId?: unknown }).materialId === 'string' &&
31
+ ((row as { kind?: unknown }).kind === 'item' ||
32
+ (row as { kind?: unknown }).kind === 'fluid');
33
+
34
+ const isWireRow = (row: unknown): boolean =>
35
+ !!row &&
36
+ typeof row === 'object' &&
37
+ typeof (row as { from?: unknown }).from === 'string' &&
38
+ typeof (row as { to?: unknown }).to === 'string' &&
39
+ typeof (row as { input?: unknown }).input === 'number';
40
+
41
+ export function isCalcState(value: unknown): value is CalculatorState {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
43
+ const row = value as Record<string, unknown>;
44
+ return (
45
+ Array.isArray(row.cards) &&
46
+ row.cards.every(isCardRow) &&
47
+ Array.isArray(row.wires) &&
48
+ row.wires.every(isWireRow) &&
49
+ !!row.tagSelections &&
50
+ typeof row.tagSelections === 'object'
51
+ );
52
+ }
53
+
54
+ export function migrateCalcState(state: CalculatorState): CalculatorState {
55
+ const cards = Array.isArray(state.cards) ? state.cards.filter(isCardRow) : [];
56
+ const ids = new Set(cards.map((card) => card.id));
57
+ return {
58
+ cards,
59
+ wires: (Array.isArray(state.wires) ? state.wires.filter(isWireRow) : []).filter(
60
+ (wire) => ids.has(wire.from) && ids.has(wire.to),
61
+ ),
62
+ tagSelections: state.tagSelections,
63
+ };
64
+ }
65
+
66
+ function isEntry(value: unknown): value is CalcTreeEntry {
67
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
68
+ const row = value as Record<string, unknown>;
69
+ const icon = row.icon as Record<string, unknown> | undefined;
70
+ return (
71
+ typeof row.id === 'string' &&
72
+ typeof row.savedAt === 'number' &&
73
+ isCalcState(row.state) &&
74
+ (icon === undefined ||
75
+ (typeof icon === 'object' &&
76
+ icon !== null &&
77
+ typeof icon.id === 'string' &&
78
+ (icon.kind === 'item' || icon.kind === 'fluid')))
79
+ );
80
+ }
81
+
82
+ export const isCalcTabs = (v: unknown): v is CalcTreeEntry[] =>
83
+ Array.isArray(v) && v.every(isEntry);
84
+ export const isCalcHistory = isCalcTabs;
85
+
86
+ export function capHistory(entries: CalcTreeEntry[]): CalcTreeEntry[] {
87
+ const seen = new Set<string>();
88
+ const out: CalcTreeEntry[] = [];
89
+ for (const entry of [...entries].sort((a, b) => b.savedAt - a.savedAt)) {
90
+ if (seen.has(entry.id)) continue;
91
+ seen.add(entry.id);
92
+ out.push(entry);
93
+ if (out.length >= MAX_HISTORY) break;
94
+ }
95
+ return out;
96
+ }
97
+