|
|
@@ -2,42 +2,58 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
import ClockTimeline from "./assets/ClockTimeline";
|
|
|
import { buildBlueprint } from "./blueprint/Blueprintbuilder";
|
|
|
import { encodeBlueprintFileBrowser } from "./blueprint/parser";
|
|
|
-import type { ClockBlock, } from "./assets/types";
|
|
|
+import type { ClockBlock } from "./assets/types";
|
|
|
import styles from "./ClockBuilder.module.css";
|
|
|
import SelectedBlockPanel from "./assets/components/SelectedBlockPanel";
|
|
|
import SelectSignal from "./assets/SelectSignal";
|
|
|
import ExpressionInput from "./assets/components/ExpressionInpux";
|
|
|
import { useClockStore } from "./store/useClockStore";
|
|
|
-
|
|
|
+import ClockWizard from "./assets/components/ClockWizard";
|
|
|
|
|
|
export default function ClockBuilder() {
|
|
|
// Subscribe to the Global Store
|
|
|
const {
|
|
|
- duration, setDuration,
|
|
|
- clockSignal, setClockSignal,
|
|
|
- rows, rowOrder,
|
|
|
+ duration,
|
|
|
+ setDuration,
|
|
|
+ clockSignal,
|
|
|
+ setClockSignal,
|
|
|
+ rows,
|
|
|
+ rowOrder,
|
|
|
blocks,
|
|
|
selectedBlockIds,
|
|
|
- addBlocks, removeBlocks,
|
|
|
- loadState
|
|
|
+ addBlocks,
|
|
|
+ removeBlocks,
|
|
|
+ loadState,
|
|
|
} = useClockStore();
|
|
|
|
|
|
// Local UI State (Keep this in the component!)
|
|
|
const [displayUnit, setDisplayUnit] = useState<"s" | "m">("s");
|
|
|
- const [beltReference, setBeltReference] = useState({ name: "turbo-belt", itemsPerSecond: 240 });
|
|
|
+ const [beltReference, setBeltReference] = useState({
|
|
|
+ name: "turbo-belt",
|
|
|
+ itemsPerSecond: 240,
|
|
|
+ });
|
|
|
const [output, setOutput] = useState("");
|
|
|
- const [status, setStatus] = useState<{ text: string; ok: boolean } | null>(null);
|
|
|
+ const [status, setStatus] = useState<{ text: string; ok: boolean } | null>(
|
|
|
+ null,
|
|
|
+ );
|
|
|
|
|
|
// Derived Data for Exports/Generation
|
|
|
// We convert our normalized Objects back to Arrays on the fly for the Factorio generator
|
|
|
- const rowsArray = useMemo(() => rowOrder.map(id => rows[id]), [rows, rowOrder]);
|
|
|
+ const rowsArray = useMemo(
|
|
|
+ () => rowOrder.map((id) => rows[id]),
|
|
|
+ [rows, rowOrder],
|
|
|
+ );
|
|
|
const blocksArray = useMemo(() => Object.values(blocks), [blocks]);
|
|
|
const totalCombinators = useMemo(() => rowsArray.length + 1, [rowsArray]);
|
|
|
|
|
|
// Keyboard Shortcuts (Ctrl+D and Delete)
|
|
|
useEffect(() => {
|
|
|
const isEditable = (el: Element | null) =>
|
|
|
- !!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" || (el as HTMLElement).isContentEditable);
|
|
|
+ !!el &&
|
|
|
+ (el.tagName === "INPUT" ||
|
|
|
+ el.tagName === "TEXTAREA" ||
|
|
|
+ el.tagName === "SELECT" ||
|
|
|
+ (el as HTMLElement).isContentEditable);
|
|
|
|
|
|
const handler = (e: KeyboardEvent) => {
|
|
|
if (isEditable(document.activeElement)) return;
|
|
|
@@ -45,7 +61,7 @@ export default function ClockBuilder() {
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "d") {
|
|
|
e.preventDefault();
|
|
|
if (selectedBlockIds.size === 0) return;
|
|
|
-
|
|
|
+
|
|
|
const clones: ClockBlock[] = [];
|
|
|
selectedBlockIds.forEach((id) => {
|
|
|
const b = blocks[id];
|
|
|
@@ -57,7 +73,7 @@ export default function ClockBuilder() {
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// Use the new batch action
|
|
|
addBlocks(clones);
|
|
|
} else if (e.key === "Delete") {
|
|
|
@@ -74,8 +90,15 @@ export default function ClockBuilder() {
|
|
|
// File I/O
|
|
|
const handleSaveFile = () => {
|
|
|
// We save the original array format for backward compatibility
|
|
|
- const config = { duration, clockSignal, rows: rowsArray, blocks: blocksArray };
|
|
|
- const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" });
|
|
|
+ const config = {
|
|
|
+ duration,
|
|
|
+ clockSignal,
|
|
|
+ rows: rowsArray,
|
|
|
+ blocks: blocksArray,
|
|
|
+ };
|
|
|
+ const blob = new Blob([JSON.stringify(config, null, 2)], {
|
|
|
+ type: "application/json",
|
|
|
+ });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement("a");
|
|
|
a.href = url;
|
|
|
@@ -92,10 +115,14 @@ export default function ClockBuilder() {
|
|
|
reader.onload = () => {
|
|
|
try {
|
|
|
const parsed = JSON.parse(reader.result as string);
|
|
|
-
|
|
|
+
|
|
|
// Re-normalize the saved array data before injecting it into the store
|
|
|
- const loadedRows = Object.fromEntries(parsed.rows.map((r: any) => [r.id, r]));
|
|
|
- const loadedBlocks = Object.fromEntries(parsed.blocks.map((b: any) => [b.id, b]));
|
|
|
+ const loadedRows = Object.fromEntries(
|
|
|
+ parsed.rows.map((r: any) => [r.id, r]),
|
|
|
+ );
|
|
|
+ const loadedBlocks = Object.fromEntries(
|
|
|
+ parsed.blocks.map((b: any) => [b.id, b]),
|
|
|
+ );
|
|
|
const loadedRowOrder = parsed.rows.map((r: any) => r.id);
|
|
|
|
|
|
loadState({
|
|
|
@@ -104,9 +131,9 @@ export default function ClockBuilder() {
|
|
|
rows: loadedRows,
|
|
|
rowOrder: loadedRowOrder,
|
|
|
blocks: loadedBlocks,
|
|
|
- selectedBlockIds: new Set()
|
|
|
+ selectedBlockIds: new Set(),
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
setStatus({ text: "Clock loaded.", ok: true });
|
|
|
} catch {
|
|
|
setStatus({ text: "That file isn't a valid clock export.", ok: false });
|
|
|
@@ -119,15 +146,26 @@ export default function ClockBuilder() {
|
|
|
// Factorio Blueprint Generation
|
|
|
const handleGenerate = () => {
|
|
|
if (rowsArray.length === 0 || blocksArray.length === 0) {
|
|
|
- setStatus({ text: "Add at least one row and one block first.", ok: false });
|
|
|
+ setStatus({
|
|
|
+ text: "Add at least one row and one block first.",
|
|
|
+ ok: false,
|
|
|
+ });
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
// Passes the derived arrays to the existing blueprint logic
|
|
|
- const bp = buildBlueprint({ duration, clockSignal, rows: rowsArray, blocks: blocksArray });
|
|
|
+ const bp = buildBlueprint({
|
|
|
+ duration,
|
|
|
+ clockSignal,
|
|
|
+ rows: rowsArray,
|
|
|
+ blocks: blocksArray,
|
|
|
+ });
|
|
|
const str = encodeBlueprintFileBrowser(bp);
|
|
|
setOutput(str);
|
|
|
- setStatus({ text: `Generated — ${bp.blueprint.entities.length} combinators.`, ok: true });
|
|
|
+ setStatus({
|
|
|
+ text: `Generated — ${bp.blueprint.entities.length} combinators.`,
|
|
|
+ ok: true,
|
|
|
+ });
|
|
|
} catch (err) {
|
|
|
setStatus({ text: `Error: ${(err as Error).message}`, ok: false });
|
|
|
}
|
|
|
@@ -153,11 +191,17 @@ export default function ClockBuilder() {
|
|
|
|
|
|
<div className={styles.field}>
|
|
|
<label>Clock signal icon</label>
|
|
|
- <SelectSignal value={clockSignal?.name} onSelectSignal={(_, sig) => setClockSignal(sig)} />
|
|
|
+ <SelectSignal
|
|
|
+ value={clockSignal?.name}
|
|
|
+ onSelectSignal={(_, sig) => setClockSignal(sig)}
|
|
|
+ />
|
|
|
</div>
|
|
|
<div className={styles.field}>
|
|
|
<label>Throughput unit</label>
|
|
|
- <select value={displayUnit} onChange={(e) => setDisplayUnit(e.target.value as "s" | "m")}>
|
|
|
+ <select
|
|
|
+ value={displayUnit}
|
|
|
+ onChange={(e) => setDisplayUnit(e.target.value as "s" | "m")}
|
|
|
+ >
|
|
|
<option value="s">items/s</option>
|
|
|
<option value="m">items/min</option>
|
|
|
</select>
|
|
|
@@ -167,18 +211,27 @@ export default function ClockBuilder() {
|
|
|
<input
|
|
|
type="text"
|
|
|
value={beltReference.name}
|
|
|
- onChange={(e) => setBeltReference({ ...beltReference, name: e.target.value })}
|
|
|
+ onChange={(e) =>
|
|
|
+ setBeltReference({ ...beltReference, name: e.target.value })
|
|
|
+ }
|
|
|
/>
|
|
|
<input
|
|
|
type="number"
|
|
|
value={beltReference.itemsPerSecond}
|
|
|
- onChange={(e) => setBeltReference({ ...beltReference, itemsPerSecond: Number(e.target.value) || 0 })}
|
|
|
+ onChange={(e) =>
|
|
|
+ setBeltReference({
|
|
|
+ ...beltReference,
|
|
|
+ itemsPerSecond: Number(e.target.value) || 0,
|
|
|
+ })
|
|
|
+ }
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <ClockWizard />
|
|
|
|
|
|
+ <hr className={styles.divider} />
|
|
|
<ClockTimeline />
|
|
|
-
|
|
|
+
|
|
|
<SelectedBlockPanel />
|
|
|
|
|
|
<div className={styles.footer}>
|
|
|
@@ -186,7 +239,11 @@ export default function ClockBuilder() {
|
|
|
<button className={styles.generateBtn} onClick={handleGenerate}>
|
|
|
Generate blueprint
|
|
|
</button>
|
|
|
- <button className={styles.copyBtn} onClick={handleCopy} disabled={!output}>
|
|
|
+ <button
|
|
|
+ className={styles.copyBtn}
|
|
|
+ onClick={handleCopy}
|
|
|
+ disabled={!output}
|
|
|
+ >
|
|
|
Copy string
|
|
|
</button>
|
|
|
<span className={styles.count}>
|
|
|
@@ -195,13 +252,31 @@ export default function ClockBuilder() {
|
|
|
<button className={styles.copyBtn} onClick={handleSaveFile}>
|
|
|
Save clock
|
|
|
</button>
|
|
|
- <input ref={fileInputRef} type="file" accept="application/json" hidden onChange={handleLoadFile} />
|
|
|
- <button className={styles.copyBtn} onClick={() => fileInputRef.current?.click()}>
|
|
|
+ <input
|
|
|
+ ref={fileInputRef}
|
|
|
+ type="file"
|
|
|
+ accept="application/json"
|
|
|
+ hidden
|
|
|
+ onChange={handleLoadFile}
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ className={styles.copyBtn}
|
|
|
+ onClick={() => fileInputRef.current?.click()}
|
|
|
+ >
|
|
|
Load clock
|
|
|
</button>
|
|
|
- {status && <span className={status.ok ? styles.statusOk : styles.statusErr}>{status.text}</span>}
|
|
|
+ {status && (
|
|
|
+ <span className={status.ok ? styles.statusOk : styles.statusErr}>
|
|
|
+ {status.text}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- <textarea className={styles.output} readOnly value={output} placeholder="Blueprint string will appear here…" />
|
|
|
+ <textarea
|
|
|
+ className={styles.output}
|
|
|
+ readOnly
|
|
|
+ value={output}
|
|
|
+ placeholder="Blueprint string will appear here…"
|
|
|
+ />
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|