@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.
- package/package.json +62 -3
- package/src/components/button-copy.tsx +43 -0
- package/src/components/cards/card/index.tsx +214 -0
- package/src/components/cards/card/scene.tsx +107 -0
- package/src/components/cards/card/strip.tsx +198 -0
- package/src/components/cards/card/text.tsx +46 -0
- package/src/components/cards/card/tips.tsx +63 -0
- package/src/components/cards/default-item.tsx +47 -0
- package/src/components/cards/glare-filter.tsx +29 -0
- package/src/components/cards/paged-card.tsx +222 -0
- package/src/components/cards/slot-hitbox.tsx +213 -0
- package/src/components/cards/strip-card/buttons.tsx +106 -0
- package/src/components/cards/strip-card/index.tsx +214 -0
- package/src/components/cards/strip-card/member-cell.tsx +167 -0
- package/src/components/cards/strip-card/sprite.tsx +36 -0
- package/src/components/embed-card.tsx +44 -0
- package/src/components/mdx/recipe.tsx +129 -0
- package/src/components/tool-list.tsx +49 -0
- package/src/components/tree.tsx +98 -0
- package/src/components/views/alloy/index.tsx +112 -0
- package/src/components/views/alloy/list.tsx +85 -0
- package/src/components/views/alloy/mix/index.tsx +35 -0
- package/src/components/views/alloy/mix/ratio.tsx +94 -0
- package/src/components/views/alloy/mix/rows.tsx +145 -0
- package/src/components/views/alloy/mix/status.tsx +122 -0
- package/src/components/views/alloy/mix/total.tsx +132 -0
- package/src/components/views/alloy/range-bar.tsx +65 -0
- package/src/components/views/alloy/stack-bar.tsx +88 -0
- package/src/components/views/browser/axis-switcher.tsx +54 -0
- package/src/components/views/browser/card-grid.tsx +275 -0
- package/src/components/views/browser/favorite-button.tsx +46 -0
- package/src/components/views/browser/frame.tsx +112 -0
- package/src/components/views/browser/index.tsx +114 -0
- package/src/components/views/browser/item-panel/index.tsx +229 -0
- package/src/components/views/browser/item-panel/skeleton.tsx +26 -0
- package/src/components/views/browser/item-panel-grid/cell.tsx +178 -0
- package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +129 -0
- package/src/components/views/browser/item-panel-grid/index.tsx +105 -0
- package/src/components/views/browser/item-view/index.tsx +186 -0
- package/src/components/views/browser/item-view/tags.tsx +85 -0
- package/src/components/views/browser/item-view-header.tsx +131 -0
- package/src/components/views/browser/pane-tabs.tsx +61 -0
- package/src/components/views/browser/place-header.tsx +72 -0
- package/src/components/views/browser/query-controls.tsx +105 -0
- package/src/components/views/browser/recipe-item-header.tsx +60 -0
- package/src/components/views/browser/settings.tsx +52 -0
- package/src/components/views/browser/toolbar.tsx +54 -0
- package/src/components/views/browser/type-tabs.tsx +71 -0
- package/src/components/views/browser/type-view.tsx +184 -0
- package/src/components/views/browser/workstation-rail.tsx +43 -0
- package/src/components/views/calc/calculator/actions.tsx +47 -0
- package/src/components/views/calc/calculator/help.tsx +189 -0
- package/src/components/views/calc/calculator/index.tsx +172 -0
- package/src/components/views/calc/dot-overlay.tsx +80 -0
- package/src/components/views/calc/edge.tsx +152 -0
- package/src/components/views/calc/flow/index.tsx +305 -0
- package/src/components/views/calc/flow/picker-dialogs.tsx +146 -0
- package/src/components/views/calc/flow/toolbar.tsx +60 -0
- package/src/components/views/calc/flow/view-menu.tsx +117 -0
- package/src/components/views/calc/member-picker.tsx +120 -0
- package/src/components/views/calc/node-card/actions.tsx +90 -0
- package/src/components/views/calc/node-card/badges.tsx +59 -0
- package/src/components/views/calc/node-card/chips.tsx +187 -0
- package/src/components/views/calc/node-card/far.tsx +123 -0
- package/src/components/views/calc/node-card/full.tsx +332 -0
- package/src/components/views/calc/node-card/index.tsx +27 -0
- package/src/components/views/calc/node-card/menu.tsx +282 -0
- package/src/components/views/calc/node-card/port-cells.tsx +186 -0
- package/src/components/views/calc/node-card/port-rails.tsx +251 -0
- package/src/components/views/calc/node-card/ports.tsx +86 -0
- package/src/components/views/calc/recipe-picker/body.tsx +207 -0
- package/src/components/views/calc/recipe-picker/footer.tsx +131 -0
- package/src/components/views/calc/recipe-picker/index.tsx +52 -0
- package/src/components/views/calc/sidebar.tsx +171 -0
- package/src/components/views/calc/trees/icon-picker.tsx +97 -0
- package/src/components/views/calc/trees/index.tsx +134 -0
- package/src/components/views/calc/trees/label.tsx +67 -0
- package/src/components/views/calc/trees/rows.tsx +140 -0
- package/src/components/views/forge/gui.tsx +131 -0
- package/src/components/views/forge/index.tsx +112 -0
- package/src/components/views/forge/list.tsx +113 -0
- package/src/components/views/forge/workbench/controls.tsx +103 -0
- package/src/components/views/forge/workbench/index.tsx +85 -0
- package/src/components/views/forge/workbench/panel.tsx +228 -0
- package/src/components/views/forge/workbench/ruler.tsx +133 -0
- package/src/components/views/forge/workbench/status.tsx +55 -0
- package/src/components/views/forge/workbench/tape.tsx +70 -0
- package/src/hooks/alloy/use-fluid-colors.ts +125 -0
- package/src/hooks/alloy/use-mix-state.ts +240 -0
- package/src/hooks/calc/flow/use-engage.ts +129 -0
- package/src/hooks/calc/flow/use-handlers.ts +192 -0
- package/src/hooks/calc/flow/use-layout.ts +105 -0
- package/src/hooks/calc/flow/use-mount.ts +97 -0
- package/src/hooks/calc/flow/use-nodes.ts +260 -0
- package/src/hooks/calc/flow/use-pickers.ts +57 -0
- package/src/hooks/calc/flow/use-viewport.ts +141 -0
- package/src/hooks/calc/flow/use-window.ts +87 -0
- package/src/hooks/calc/use-actions.ts +355 -0
- package/src/hooks/calc/use-amounts.ts +64 -0
- package/src/hooks/calc/use-data.ts +203 -0
- package/src/hooks/calc/use-defaults.ts +40 -0
- package/src/hooks/calc/use-far-mode.ts +37 -0
- package/src/hooks/calc/use-focus.ts +31 -0
- package/src/hooks/calc/use-maps.ts +76 -0
- package/src/hooks/calc/use-reset.ts +15 -0
- package/src/hooks/calc/use-resolver.ts +70 -0
- package/src/hooks/calc/use-session.ts +13 -0
- package/src/hooks/calc/use-share.ts +64 -0
- package/src/hooks/calc/use-storage.ts +126 -0
- package/src/hooks/calc/use-tier.ts +13 -0
- package/src/hooks/calc/use-view-mode.ts +24 -0
- package/src/hooks/calc/use-worker.ts +204 -0
- package/src/hooks/forge/use-anvil-gui.ts +16 -0
- package/src/hooks/forge/use-forge-state.ts +135 -0
- package/src/hooks/forge/use-ruler.ts +100 -0
- package/src/hooks/use-add-to-calc.ts +51 -0
- package/src/hooks/use-categories-by-id.ts +18 -0
- package/src/hooks/use-favorite-hotkey.ts +39 -0
- package/src/hooks/use-favorites.ts +135 -0
- package/src/hooks/use-group-outlines.ts +65 -0
- package/src/hooks/use-hide-unused.ts +12 -0
- package/src/hooks/use-index.ts +72 -0
- package/src/hooks/use-item-feeds.ts +46 -0
- package/src/hooks/use-item-pane.ts +106 -0
- package/src/hooks/use-item-press.ts +89 -0
- package/src/hooks/use-item-tag-groups.ts +54 -0
- package/src/hooks/use-item-tooltips.ts +18 -0
- package/src/hooks/use-left-sidebar-tree.ts +34 -0
- package/src/hooks/use-mod-names.ts +17 -0
- package/src/hooks/use-mount-limit.ts +22 -0
- package/src/hooks/use-panel-cells.ts +117 -0
- package/src/hooks/use-panel-page.ts +54 -0
- package/src/hooks/use-panel-tabs.ts +43 -0
- package/src/hooks/use-query.ts +186 -0
- package/src/hooks/use-session.ts +88 -0
- package/src/hooks/use-sidebar-group.ts +16 -0
- package/src/hooks/use-tag-members.ts +16 -0
- package/src/hooks/use-tool-recipes.ts +26 -0
- package/src/hooks/use-type-filter.ts +73 -0
- package/src/lib/alloy/mix.ts +210 -0
- package/src/lib/alloy/parse.ts +37 -0
- package/src/lib/calc/defaults.ts +47 -0
- package/src/lib/calc/flow.ts +315 -0
- package/src/lib/calc/focus.ts +15 -0
- package/src/lib/calc/meta.ts +17 -0
- package/src/lib/calc/pack.ts +77 -0
- package/src/lib/calc/ports.ts +43 -0
- package/src/lib/calc/resolve.ts +198 -0
- package/src/lib/calc/route/buckets.ts +49 -0
- package/src/lib/calc/route/geometry.ts +51 -0
- package/src/lib/calc/route/index.ts +3 -0
- package/src/lib/calc/route/polyline.ts +103 -0
- package/src/lib/calc/route/router.ts +394 -0
- package/src/lib/calc/scheme.ts +226 -0
- package/src/lib/calc/storage/entry.ts +97 -0
- package/src/lib/calc/storage/index.ts +20 -0
- package/src/lib/calc/storage/keys.ts +38 -0
- package/src/lib/calc/storage/store.ts +198 -0
- package/src/lib/calc/tree/index.ts +4 -0
- package/src/lib/calc/tree/materials.ts +29 -0
- package/src/lib/calc/tree/recipe.ts +104 -0
- package/src/lib/calc/tree/share.ts +135 -0
- package/src/lib/calc/worker/api.ts +25 -0
- package/src/lib/calc/worker/fetch.ts +66 -0
- package/src/lib/calc/worker/flush.ts +39 -0
- package/src/lib/calc/worker/index.ts +8 -0
- package/src/lib/calc/worker/protocol.ts +35 -0
- package/src/lib/calc/worker/resolve.ts +49 -0
- package/src/lib/calc/worker/state.ts +48 -0
- package/src/lib/card-model.ts +65 -0
- package/src/lib/client/recipe-categories.ts +14 -0
- package/src/lib/client/recipe.ts +85 -0
- package/src/lib/client-index/catalog.ts +91 -0
- package/src/lib/client-index/chrome.ts +180 -0
- package/src/lib/client-index/index.ts +20 -0
- package/src/lib/client-index/registry.ts +121 -0
- package/src/lib/client-index/rows.ts +260 -0
- package/src/lib/client-index/tooltips.ts +74 -0
- package/src/lib/config.ts +18 -0
- package/src/lib/data/category/dto.ts +25 -0
- package/src/lib/data/category/load.ts +47 -0
- package/src/lib/data/category/parse.ts +71 -0
- package/src/lib/data/dto.ts +152 -0
- package/src/lib/data/grid.ts +40 -0
- package/src/lib/data/parse/entry.ts +85 -0
- package/src/lib/data/parse/index.ts +3 -0
- package/src/lib/data/parse/layout.ts +138 -0
- package/src/lib/data/parse/slots.ts +85 -0
- package/src/lib/data/parse/widgets.ts +81 -0
- package/src/lib/data/scene.ts +103 -0
- package/src/lib/data/slot-io.ts +44 -0
- package/src/lib/dump.ts +6 -0
- package/src/lib/embed.ts +22 -0
- package/src/lib/favorite-hover.ts +13 -0
- package/src/lib/fluid-outputs.ts +70 -0
- package/src/lib/forge/actions.ts +59 -0
- package/src/lib/forge/data.ts +51 -0
- package/src/lib/forge/solve.ts +80 -0
- package/src/lib/group.ts +23 -0
- package/src/lib/index-store.ts +70 -0
- package/src/lib/links.ts +131 -0
- package/src/lib/lookup.ts +119 -0
- package/src/lib/panel-outline.ts +86 -0
- package/src/lib/panel.ts +105 -0
- package/src/lib/preview.ts +32 -0
- package/src/lib/recipe-slots.ts +8 -0
- package/src/lib/search.ts +139 -0
- package/src/lib/sidebar.ts +47 -0
- package/src/lib/used-items.ts +25 -0
- package/src/locales/en_us.json +172 -0
- package/src/locales/ru_ru.json +172 -0
- package/src/locales.ts +6 -0
- package/src/types/alloy.ts +20 -0
- package/src/types/calc-flow-props.ts +44 -0
- package/src/types/calc.ts +92 -0
- package/src/types/forge.ts +21 -0
- package/src/types/index.ts +136 -0
- package/styles/calc.css +308 -0
- package/styles/strip.css +7 -0
- package/styles.css +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { asStringList } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
|
|
3
|
+
import type { RecipeCompactIndex, RecipeCompactRow } from '@terrafirmagreg-team/recipe/types';
|
|
4
|
+
import { registryId, splitDumpId } from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
5
|
+
import { resolveClientTag } from '@terrafirmagreg-team/dump/lib/dump/client/tags';
|
|
6
|
+
import {
|
|
7
|
+
loadRegistryEntrySlice,
|
|
8
|
+
loadTagSlice,
|
|
9
|
+
loadTypeRows,
|
|
10
|
+
resolveRecipeRows,
|
|
11
|
+
} from '@terrafirmagreg-team/recipe/lib/client-index';
|
|
12
|
+
import { stackIdFromRef } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
13
|
+
import { loadFluidIoIndex } from '@terrafirmagreg-team/recipe/lib/fluid-outputs';
|
|
14
|
+
|
|
15
|
+
export { registryId, tagPseudoId } from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
16
|
+
|
|
17
|
+
function uniqueRows(rows: RecipeCompactRow[]): RecipeCompactRow[] {
|
|
18
|
+
const seen = new Set<string>();
|
|
19
|
+
return rows.filter((row) => {
|
|
20
|
+
if (seen.has(row.id)) return false;
|
|
21
|
+
seen.add(row.id);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ALL_KINDS = ['item', 'block', 'fluid', 'entity'];
|
|
27
|
+
|
|
28
|
+
async function entryEdges(
|
|
29
|
+
dataset: DatasetRef,
|
|
30
|
+
kinds: string[],
|
|
31
|
+
id: string,
|
|
32
|
+
field: 'usedBy' | 'producedBy',
|
|
33
|
+
): Promise<string[]> {
|
|
34
|
+
const edges = await Promise.all(
|
|
35
|
+
kinds.map((kind) =>
|
|
36
|
+
loadRegistryEntrySlice(dataset, kind, id).then((entry) =>
|
|
37
|
+
asStringList(entry?.[field]),
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
return [...new Set(edges.flat())];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function recipesForOutput(
|
|
45
|
+
dataset: DatasetRef,
|
|
46
|
+
index: RecipeCompactIndex,
|
|
47
|
+
item: string,
|
|
48
|
+
opts?: { includeTagRows?: boolean },
|
|
49
|
+
): Promise<RecipeCompactRow[]> {
|
|
50
|
+
const base = registryId(item);
|
|
51
|
+
const [refs, io] = await Promise.all([
|
|
52
|
+
entryEdges(dataset, ALL_KINDS, base, 'producedBy'),
|
|
53
|
+
index.fluids?.includes(base)
|
|
54
|
+
? loadFluidIoIndex(dataset, index).catch(() => undefined)
|
|
55
|
+
: undefined,
|
|
56
|
+
]);
|
|
57
|
+
const extra = io?.outputs.get(base) ?? [];
|
|
58
|
+
const ids = extra.length ? [...new Set([...refs, ...extra])] : refs;
|
|
59
|
+
return resolveRecipeRows(dataset, index, ids, opts);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function usesForInput(
|
|
63
|
+
dataset: DatasetRef,
|
|
64
|
+
index: RecipeCompactIndex,
|
|
65
|
+
item: string,
|
|
66
|
+
kind: string = 'item',
|
|
67
|
+
opts?: { includeTagRows?: boolean },
|
|
68
|
+
): Promise<RecipeCompactRow[]> {
|
|
69
|
+
const base = registryId(item);
|
|
70
|
+
const ns = splitDumpId(base)?.ns;
|
|
71
|
+
if (!ns) return [];
|
|
72
|
+
const tagKinds = [...new Set(kind === 'item' ? ['item', 'block'] : [kind])];
|
|
73
|
+
const stationTypes = (index.categories ?? [])
|
|
74
|
+
.filter((cat) =>
|
|
75
|
+
(cat.workstations ?? []).some(
|
|
76
|
+
(ref) => registryId(stackIdFromRef(ref)) === base,
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
.map((cat) => cat.id);
|
|
80
|
+
const [refs, io, stationRows, ...tagSlices] = await Promise.all([
|
|
81
|
+
entryEdges(dataset, ALL_KINDS, base, 'usedBy'),
|
|
82
|
+
index.fluids?.includes(base)
|
|
83
|
+
? loadFluidIoIndex(dataset, index).catch(() => undefined)
|
|
84
|
+
: undefined,
|
|
85
|
+
Promise.all(
|
|
86
|
+
stationTypes.map((typeId) => loadTypeRows(dataset, index, typeId)),
|
|
87
|
+
),
|
|
88
|
+
...tagKinds.map((tagKind) => loadTagSlice(dataset, tagKind, ns)),
|
|
89
|
+
]);
|
|
90
|
+
const tagKeys: string[] = [];
|
|
91
|
+
for (const [i, slice] of tagSlices.entries()) {
|
|
92
|
+
const registry = `${tagKinds[i]}s`;
|
|
93
|
+
for (const tag of slice[base] ?? slice[item] ?? []) {
|
|
94
|
+
const raw = tag.startsWith('#') ? tag.slice(1) : tag;
|
|
95
|
+
const parts = splitDumpId(raw);
|
|
96
|
+
if (parts) tagKeys.push(`${parts.ns}:${registry}/${parts.path}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const tagRefs = (
|
|
100
|
+
await Promise.all(
|
|
101
|
+
[...new Set(tagKeys)].map(async (key) => {
|
|
102
|
+
const parts = splitDumpId(key);
|
|
103
|
+
const tag = parts
|
|
104
|
+
? await resolveClientTag(dataset, `${parts.ns}:${parts.path}`)
|
|
105
|
+
: undefined;
|
|
106
|
+
return tag?.usedBy ?? [];
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
).flat();
|
|
110
|
+
const ioRefs = io?.inputs.get(base) ?? [];
|
|
111
|
+
const allRefs = ioRefs.length
|
|
112
|
+
? [...new Set([...refs, ...ioRefs])]
|
|
113
|
+
: refs;
|
|
114
|
+
return uniqueRows([
|
|
115
|
+
...(await resolveRecipeRows(dataset, index, [...allRefs, ...tagRefs], opts)),
|
|
116
|
+
...stationRows.flat(),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type OutlineRect = { x: number; y: number; w: number; h: number };
|
|
2
|
+
|
|
3
|
+
type GroupOutline = {
|
|
4
|
+
groupId: string;
|
|
5
|
+
d: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const ptKey = (x: number, y: number) => `${x.toFixed(2)},${y.toFixed(2)}`;
|
|
9
|
+
const edgeKey = (x1: number, y1: number, x2: number, y2: number) =>
|
|
10
|
+
`${ptKey(x1, y1)}>${ptKey(x2, y2)}`;
|
|
11
|
+
|
|
12
|
+
export function groupOutlines(
|
|
13
|
+
indicesByGroup: Map<string, number[]>,
|
|
14
|
+
cols: number,
|
|
15
|
+
cell: OutlineRect,
|
|
16
|
+
gap: number,
|
|
17
|
+
bounds?: { w: number; h: number },
|
|
18
|
+
stroke = 0,
|
|
19
|
+
): GroupOutline[] {
|
|
20
|
+
const pad = gap / 2;
|
|
21
|
+
const inset = stroke / 2;
|
|
22
|
+
const maxX = (bounds?.w ?? Number.POSITIVE_INFINITY) - inset;
|
|
23
|
+
const maxY = (bounds?.h ?? Number.POSITIVE_INFINITY) - inset;
|
|
24
|
+
const out: GroupOutline[] = [];
|
|
25
|
+
for (const [groupId, indices] of indicesByGroup) {
|
|
26
|
+
const cells = new Set(
|
|
27
|
+
indices.map((i) => `${i % cols},${Math.floor(i / cols)}`),
|
|
28
|
+
);
|
|
29
|
+
const segs: [number, number, number, number][] = [];
|
|
30
|
+
for (const i of indices) {
|
|
31
|
+
const col = i % cols;
|
|
32
|
+
const row = Math.floor(i / cols);
|
|
33
|
+
const x = Math.max(cell.x + col * (cell.w + gap) - pad, inset);
|
|
34
|
+
const y = Math.max(cell.y + row * (cell.h + gap) - pad, inset);
|
|
35
|
+
const w =
|
|
36
|
+
Math.min(cell.x + col * (cell.w + gap) + cell.w + pad, maxX) - x;
|
|
37
|
+
const h =
|
|
38
|
+
Math.min(cell.y + row * (cell.h + gap) + cell.h + pad, maxY) - y;
|
|
39
|
+
if (!cells.has(`${col},${row - 1}`)) segs.push([x, y, x + w, y]);
|
|
40
|
+
if (!cells.has(`${col},${row + 1}`)) segs.push([x, y + h, x + w, y + h]);
|
|
41
|
+
if (!cells.has(`${col - 1},${row}`)) segs.push([x, y, x, y + h]);
|
|
42
|
+
if (!cells.has(`${col + 1},${row}`)) segs.push([x + w, y, x + w, y + h]);
|
|
43
|
+
}
|
|
44
|
+
const adj = new Map<string, { x: number; y: number }[]>();
|
|
45
|
+
for (const [x1, y1, x2, y2] of segs) {
|
|
46
|
+
let a = adj.get(ptKey(x1, y1));
|
|
47
|
+
if (!a) adj.set(ptKey(x1, y1), (a = []));
|
|
48
|
+
a.push({ x: x2, y: y2 });
|
|
49
|
+
let b = adj.get(ptKey(x2, y2));
|
|
50
|
+
if (!b) adj.set(ptKey(x2, y2), (b = []));
|
|
51
|
+
b.push({ x: x1, y: y1 });
|
|
52
|
+
}
|
|
53
|
+
const used = new Set<string>();
|
|
54
|
+
for (const [x1, y1, x2, y2] of segs) {
|
|
55
|
+
const forward = edgeKey(x1, y1, x2, y2);
|
|
56
|
+
if (used.has(forward)) continue;
|
|
57
|
+
used.add(forward);
|
|
58
|
+
used.add(edgeKey(x2, y2, x1, y1));
|
|
59
|
+
const loop: [number, number][] = [
|
|
60
|
+
[x1, y1],
|
|
61
|
+
[x2, y2],
|
|
62
|
+
];
|
|
63
|
+
let cx = x2;
|
|
64
|
+
let cy = y2;
|
|
65
|
+
while (cx !== x1 || cy !== y1) {
|
|
66
|
+
const next = (adj.get(ptKey(cx, cy)) ?? []).find(
|
|
67
|
+
(p) => !used.has(edgeKey(cx, cy, p.x, p.y)),
|
|
68
|
+
);
|
|
69
|
+
if (!next) break;
|
|
70
|
+
used.add(edgeKey(cx, cy, next.x, next.y));
|
|
71
|
+
used.add(edgeKey(next.x, next.y, cx, cy));
|
|
72
|
+
loop.push([next.x, next.y]);
|
|
73
|
+
cx = next.x;
|
|
74
|
+
cy = next.y;
|
|
75
|
+
}
|
|
76
|
+
if (loop.length > 2) {
|
|
77
|
+
out.push({
|
|
78
|
+
groupId,
|
|
79
|
+
d: `M${loop.map(([x, y]) => `${x.toFixed(2)} ${y.toFixed(2)}`).join(' L')} Z`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
package/src/lib/panel.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stackIdFromRef,
|
|
3
|
+
type TaggedReference,
|
|
4
|
+
} from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
5
|
+
import { registryId } from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
6
|
+
import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
|
|
7
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
8
|
+
import type {
|
|
9
|
+
ItemPanelCell,
|
|
10
|
+
RecipeCompactIndex,
|
|
11
|
+
RecipeStackGroup,
|
|
12
|
+
} from '@terrafirmagreg-team/recipe/types';
|
|
13
|
+
|
|
14
|
+
function catalogPool(index: RecipeCompactIndex, tab?: string): string[] {
|
|
15
|
+
if (!tab) return index.items;
|
|
16
|
+
return index.tabItems?.[tab] ?? [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ItemPanelMode = 'all' | 'fav';
|
|
20
|
+
|
|
21
|
+
type ItemPanelTabRow = {
|
|
22
|
+
id: string;
|
|
23
|
+
icon: string;
|
|
24
|
+
iconRef?: TaggedReference;
|
|
25
|
+
help: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function panelPool(
|
|
29
|
+
mode: ItemPanelMode,
|
|
30
|
+
favoriteIds: string[],
|
|
31
|
+
index: RecipeCompactIndex | null,
|
|
32
|
+
tab?: string,
|
|
33
|
+
): string[] {
|
|
34
|
+
if (!index) return [];
|
|
35
|
+
return mode === 'fav' ? favoriteIds : catalogPool(index, tab);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildPanelCells(
|
|
39
|
+
ids: readonly string[],
|
|
40
|
+
groups: RecipeStackGroup[] | undefined,
|
|
41
|
+
expandedGroups: ReadonlySet<string>,
|
|
42
|
+
): ItemPanelCell[] {
|
|
43
|
+
if (!groups?.length) return ids.map((id) => ({ kind: 'stack', id }));
|
|
44
|
+
const byMember = new Map<string, RecipeStackGroup>();
|
|
45
|
+
for (const group of groups)
|
|
46
|
+
for (const member of group.members) {
|
|
47
|
+
const base = registryId(member);
|
|
48
|
+
if (!byMember.has(base)) byMember.set(base, group);
|
|
49
|
+
}
|
|
50
|
+
const inPool = new Set(ids.map(registryId));
|
|
51
|
+
const seen = new Set<string>();
|
|
52
|
+
const out: ItemPanelCell[] = [];
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
const base = registryId(id);
|
|
55
|
+
if (seen.has(base)) continue;
|
|
56
|
+
seen.add(base);
|
|
57
|
+
const group = byMember.get(base);
|
|
58
|
+
if (!group) {
|
|
59
|
+
out.push({ kind: 'stack', id });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const members = group.members.filter((member) =>
|
|
63
|
+
inPool.has(registryId(member)),
|
|
64
|
+
);
|
|
65
|
+
for (const member of members) seen.add(registryId(member));
|
|
66
|
+
if (members.length <= 1) {
|
|
67
|
+
out.push({ kind: 'stack', id: members[0] ?? id });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const expanded = expandedGroups.has(group.id);
|
|
71
|
+
out.push({
|
|
72
|
+
kind: 'group',
|
|
73
|
+
id: group.id,
|
|
74
|
+
label: group.name ?? group.id,
|
|
75
|
+
members,
|
|
76
|
+
expanded,
|
|
77
|
+
});
|
|
78
|
+
if (expanded)
|
|
79
|
+
for (const member of members)
|
|
80
|
+
out.push({ kind: 'stack', id: member, groupId: group.id });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function panelTabRows(
|
|
86
|
+
index: RecipeCompactIndex | null,
|
|
87
|
+
names: Record<string, Record<string, string>>,
|
|
88
|
+
lang: Lang,
|
|
89
|
+
): ItemPanelTabRow[] {
|
|
90
|
+
const rows: ItemPanelTabRow[] = [];
|
|
91
|
+
for (const row of index?.tabs ?? []) {
|
|
92
|
+
const icon =
|
|
93
|
+
(row.icon ? stackIdFromRef(row.icon) : undefined) ??
|
|
94
|
+
index?.tabItems?.[row.id]?.[0];
|
|
95
|
+
if (!icon) continue;
|
|
96
|
+
rows.push({
|
|
97
|
+
id: row.id,
|
|
98
|
+
icon,
|
|
99
|
+
iconRef: row.icon,
|
|
100
|
+
help: recipeItemName(names, lang, row.display.name ?? row.id),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerPreviewResolver } from '@terrafirmagreg-team/preview/lib';
|
|
2
|
+
import { clientDumpLang } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
|
|
3
|
+
import { loadDatasetRecipeCategories } from '@terrafirmagreg-team/recipe/lib/data/category/load';
|
|
4
|
+
import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
5
|
+
import { entryPreviewIcon } from '@terrafirmagreg-team/game/lib/preview';
|
|
6
|
+
import type { PreviewResolverCtx } from '@terrafirmagreg-team/preview/lib';
|
|
7
|
+
import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
|
|
8
|
+
|
|
9
|
+
export async function recipeLinkPreview({
|
|
10
|
+
parts,
|
|
11
|
+
query,
|
|
12
|
+
langMap,
|
|
13
|
+
}: PreviewResolverCtx): Promise<PagePreviewData | undefined> {
|
|
14
|
+
const dataset = {
|
|
15
|
+
id: parts[1] ?? '',
|
|
16
|
+
version: query.get('version') ?? undefined,
|
|
17
|
+
};
|
|
18
|
+
if (!dataset.id) return undefined;
|
|
19
|
+
const type = query.get('type');
|
|
20
|
+
if (!type) return { title: 'Recipes' };
|
|
21
|
+
const categories = await loadDatasetRecipeCategories(dataset);
|
|
22
|
+
const row = categories.find((category) => category.id === type);
|
|
23
|
+
if (!row) return { title: type };
|
|
24
|
+
return {
|
|
25
|
+
title:
|
|
26
|
+
(row.display?.name && langString(langMap, row.display.name)) || row.id,
|
|
27
|
+
icon: entryPreviewIcon(row.icon, dataset),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registerPreviewResolver('recipe', recipeLinkPreview, { langMap: clientDumpLang });
|
|
32
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Recipe, RecipeSlotWidget } from '@terrafirmagreg-team/recipe/lib/data/dto';
|
|
2
|
+
|
|
3
|
+
export function recipeSlots(recipe: Recipe): RecipeSlotWidget[] {
|
|
4
|
+
return recipe.widgets.filter(
|
|
5
|
+
(widget): widget is RecipeSlotWidget => widget.type === 'slot',
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
|
|
2
|
+
|
|
3
|
+
function matchesResolvedLabel(
|
|
4
|
+
id: string,
|
|
5
|
+
label: string,
|
|
6
|
+
needle: string,
|
|
7
|
+
): boolean {
|
|
8
|
+
const q = needle.trim().toLowerCase();
|
|
9
|
+
if (!q) return true;
|
|
10
|
+
if (label.toLowerCase().includes(q)) return true;
|
|
11
|
+
const lower = id.toLowerCase();
|
|
12
|
+
return lower === q || lower.endsWith(`:${q}`) || lower.endsWith(`/${q}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ItemSearchContext = {
|
|
16
|
+
names: Record<string, Record<string, string>>;
|
|
17
|
+
lang: string;
|
|
18
|
+
tagMembers?: Record<string, string[]>;
|
|
19
|
+
mods?: Record<string, string>;
|
|
20
|
+
tooltips?: Record<string, string[]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SearchToken = {
|
|
24
|
+
negated: boolean;
|
|
25
|
+
kind: 'name' | 'mod' | 'tag' | 'tooltip';
|
|
26
|
+
needle: string;
|
|
27
|
+
regex?: RegExp;
|
|
28
|
+
memberSet?: Set<string>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const TOKEN_RE = /-?[@#$]?(?:"[^"]*"|\/(?:\\.|[^\\/])*\/|[^\s]+)/g;
|
|
32
|
+
|
|
33
|
+
function tokenize(query: string): SearchToken[][] {
|
|
34
|
+
return query
|
|
35
|
+
.split('|')
|
|
36
|
+
.map((branch) =>
|
|
37
|
+
(branch.match(TOKEN_RE) ?? [])
|
|
38
|
+
.flatMap((raw): SearchToken[] => {
|
|
39
|
+
if (!raw) return [];
|
|
40
|
+
const negated = raw.startsWith('-');
|
|
41
|
+
let token = negated ? raw.slice(1) : raw;
|
|
42
|
+
let kind: SearchToken['kind'] = 'name';
|
|
43
|
+
const prefix = token.charAt(0);
|
|
44
|
+
if (prefix === '@' || prefix === '#' || prefix === '$') {
|
|
45
|
+
kind = prefix === '@' ? 'mod' : prefix === '#' ? 'tag' : 'tooltip';
|
|
46
|
+
token = token.slice(1);
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
token.length > 1 &&
|
|
50
|
+
token.startsWith('"') &&
|
|
51
|
+
token.endsWith('"')
|
|
52
|
+
) {
|
|
53
|
+
token = token.slice(1, -1);
|
|
54
|
+
} else if (
|
|
55
|
+
token.length > 1 &&
|
|
56
|
+
token.startsWith('/') &&
|
|
57
|
+
token.endsWith('/')
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
negated,
|
|
63
|
+
kind,
|
|
64
|
+
needle: '',
|
|
65
|
+
regex: new RegExp(token.slice(1, -1), 'i'),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
const needle = token.trim().toLowerCase();
|
|
71
|
+
return needle ? [{ negated, kind, needle }] : [];
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
.filter((branch) => branch.length > 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tagMemberSet(token: SearchToken, ctx: ItemSearchContext): Set<string> {
|
|
78
|
+
const set = new Set<string>();
|
|
79
|
+
const test = (value: string) =>
|
|
80
|
+
token.regex
|
|
81
|
+
? token.regex.test(value)
|
|
82
|
+
: value.toLowerCase().includes(token.needle);
|
|
83
|
+
for (const [tag, ids] of Object.entries(ctx.tagMembers ?? {})) {
|
|
84
|
+
if (test(tag) || test(recipeItemName(ctx.names, ctx.lang, tag))) {
|
|
85
|
+
for (const id of ids) set.add(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return set;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function matchToken(
|
|
92
|
+
token: SearchToken,
|
|
93
|
+
id: string,
|
|
94
|
+
label: string,
|
|
95
|
+
ctx: ItemSearchContext,
|
|
96
|
+
): boolean {
|
|
97
|
+
const test = (value: string) =>
|
|
98
|
+
token.regex
|
|
99
|
+
? token.regex.test(value)
|
|
100
|
+
: value.toLowerCase().includes(token.needle);
|
|
101
|
+
switch (token.kind) {
|
|
102
|
+
case 'mod': {
|
|
103
|
+
const ns = id.split(':')[0] ?? '';
|
|
104
|
+
const modName = (ctx.mods?.[ns] ?? '').toLowerCase().replace(/\s+/g, '');
|
|
105
|
+
return test(modName) || test(ns);
|
|
106
|
+
}
|
|
107
|
+
case 'tag': {
|
|
108
|
+
token.memberSet ??= tagMemberSet(token, ctx);
|
|
109
|
+
return token.memberSet.has(id);
|
|
110
|
+
}
|
|
111
|
+
case 'tooltip':
|
|
112
|
+
return (ctx.tooltips?.[id] ?? []).some((line) => test(line));
|
|
113
|
+
default:
|
|
114
|
+
if (token.regex) return token.regex.test(label) || token.regex.test(id);
|
|
115
|
+
return matchesResolvedLabel(id, label, token.needle);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function matchesGameItemSearch(
|
|
120
|
+
id: string,
|
|
121
|
+
ctx: ItemSearchContext,
|
|
122
|
+
groups: SearchToken[][],
|
|
123
|
+
): boolean {
|
|
124
|
+
const label = recipeItemName(ctx.names, ctx.lang, id);
|
|
125
|
+
return groups.some((branch) =>
|
|
126
|
+
branch.every((token) => matchToken(token, id, label, ctx) !== token.negated),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function filterCatalogItemIds(
|
|
131
|
+
ids: string[],
|
|
132
|
+
query: string,
|
|
133
|
+
ctx: ItemSearchContext,
|
|
134
|
+
): string[] {
|
|
135
|
+
const groups = tokenize(query);
|
|
136
|
+
if (!groups.length) return ids;
|
|
137
|
+
return ids.filter((id) => matchesGameItemSearch(id, ctx, groups));
|
|
138
|
+
}
|
|
139
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
2
|
+
|
|
3
|
+
function typeParamFromRecipeUrl(url: string | undefined): string | undefined {
|
|
4
|
+
if (!url) return undefined;
|
|
5
|
+
const query = url.split('?')[1];
|
|
6
|
+
if (!query) return undefined;
|
|
7
|
+
return new URLSearchParams(query).get('type') ?? undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function collectRecipeLeaves(nodes: PageTree.Node[]): PageTree.Item[] {
|
|
11
|
+
const out: PageTree.Item[] = [];
|
|
12
|
+
for (const node of nodes) {
|
|
13
|
+
if (node.type === 'page') out.push(node);
|
|
14
|
+
else if (node.type === 'folder')
|
|
15
|
+
out.push(...collectRecipeLeaves(node.children));
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function groupRecipeSidebarByMod(
|
|
21
|
+
tree: PageTree.Root,
|
|
22
|
+
modNames: Record<string, string>,
|
|
23
|
+
): PageTree.Root {
|
|
24
|
+
const leaves = collectRecipeLeaves(tree.children);
|
|
25
|
+
if (!leaves.length) return tree;
|
|
26
|
+
const byMod: Record<string, PageTree.Item[]> = {};
|
|
27
|
+
for (const leaf of leaves) {
|
|
28
|
+
const mod = typeParamFromRecipeUrl(leaf.url)?.split(':', 1)[0] ?? '_';
|
|
29
|
+
const list = byMod[mod];
|
|
30
|
+
if (list) list.push(leaf);
|
|
31
|
+
else byMod[mod] = [leaf];
|
|
32
|
+
}
|
|
33
|
+
const folders: PageTree.Folder[] = Object.entries(byMod)
|
|
34
|
+
.map(([mod, children]) => ({
|
|
35
|
+
type: 'folder' as const,
|
|
36
|
+
name: modNames[mod] ?? mod,
|
|
37
|
+
children,
|
|
38
|
+
$id: `mod/${mod}`,
|
|
39
|
+
}))
|
|
40
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
41
|
+
return {
|
|
42
|
+
name: tree.name,
|
|
43
|
+
children: folders,
|
|
44
|
+
$id: `${tree.$id}:mod`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registryId } from '@terrafirmagreg-team/recipe/lib/lookup';
|
|
2
|
+
import type { RecipeCompactIndex, UsedItems } from '@terrafirmagreg-team/recipe/types';
|
|
3
|
+
|
|
4
|
+
export function recipeUsedItems(index: RecipeCompactIndex | null): UsedItems {
|
|
5
|
+
const ids = new Set<string>();
|
|
6
|
+
const bases = new Set<string>();
|
|
7
|
+
for (const id of index?.usedIds ?? []) {
|
|
8
|
+
ids.add(id);
|
|
9
|
+
bases.add(registryId(id));
|
|
10
|
+
}
|
|
11
|
+
return { ids, bases };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isRecipeUsed(id: string, used: UsedItems): boolean {
|
|
15
|
+
return used.ids.has(id) || used.bases.has(registryId(id));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function filterUsedIds(ids: string[], used: UsedItems): string[] {
|
|
19
|
+
return ids.filter((id) => isRecipeUsed(id, used));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasRecipeUsage(used: UsedItems): boolean {
|
|
23
|
+
return used.ids.size > 0;
|
|
24
|
+
}
|
|
25
|
+
|