|
@@ -1,22 +1,12 @@
|
|
|
-import { useCallback, useMemo, useRef, useState } from "react";
|
|
|
|
|
|
|
+import { useMemo } from "react";
|
|
|
import Icon from "./icon";
|
|
import Icon from "./icon";
|
|
|
-import SelectSignal, { type Signal } from "./SelectSignal";
|
|
|
|
|
import styles from "./ClockTimeline.module.css";
|
|
import styles from "./ClockTimeline.module.css";
|
|
|
-import { ACTION_PRESETS, expandBlockInstances, getPreset, type ClockBlock, type ClockRow } from "./types";
|
|
|
|
|
-import ExpressionInput from "./components/ExpressionInpux";
|
|
|
|
|
|
|
+import { ACTION_PRESETS } from "./types";
|
|
|
|
|
+import TimelineRow from "./components/TimelineRow";
|
|
|
|
|
+import { useClockStore } from "../store/useClockStore";
|
|
|
|
|
|
|
|
-type Props = {
|
|
|
|
|
- duration: number;
|
|
|
|
|
- rows: ClockRow[];
|
|
|
|
|
- blocks: ClockBlock[];
|
|
|
|
|
- selectedBlockIds: Set<string>;
|
|
|
|
|
- onRowsChange: (rows: ClockRow[]) => void;
|
|
|
|
|
- onBlocksChange: (blocks: ClockBlock[]) => void;
|
|
|
|
|
- onSelectBlocks: (ids: Set<string>) => void;
|
|
|
|
|
-};
|
|
|
|
|
|
|
+type Props = {};
|
|
|
|
|
|
|
|
-const LANE_HEIGHT = 30;
|
|
|
|
|
-const LANE_GAP = 4;
|
|
|
|
|
export const defaultActivationSignal = {
|
|
export const defaultActivationSignal = {
|
|
|
type: "virtual-signal",
|
|
type: "virtual-signal",
|
|
|
name: "signal-check",
|
|
name: "signal-check",
|
|
@@ -24,238 +14,61 @@ export const defaultActivationSignal = {
|
|
|
icon: "virtual-signal/signal-check.png",
|
|
icon: "virtual-signal/signal-check.png",
|
|
|
order: "a[checked]",
|
|
order: "a[checked]",
|
|
|
};
|
|
};
|
|
|
-let uid = 0;
|
|
|
|
|
-const nextId = (prefix: string) => `${prefix}-${++uid}-${Date.now()}`;
|
|
|
|
|
-type PackedInstance = { id: string; blockId: string; start: number; duration: number; lane: number };
|
|
|
|
|
-
|
|
|
|
|
-function packLanes(instances: { id: string; blockId: string; start: number; duration: number }[]) {
|
|
|
|
|
- const sorted = [...instances].sort((a, b) => a.start - b.start);
|
|
|
|
|
- const laneEnds: number[] = [];
|
|
|
|
|
- const packed: PackedInstance[] = [];
|
|
|
|
|
- for (const inst of sorted) {
|
|
|
|
|
- let lane = laneEnds.findIndex((end) => end <= inst.start);
|
|
|
|
|
- if (lane === -1) {
|
|
|
|
|
- lane = laneEnds.length;
|
|
|
|
|
- laneEnds.push(inst.start + inst.duration);
|
|
|
|
|
- } else {
|
|
|
|
|
- laneEnds[lane] = inst.start + inst.duration;
|
|
|
|
|
- }
|
|
|
|
|
- packed.push({ ...inst, lane });
|
|
|
|
|
- }
|
|
|
|
|
- return { packed, laneCount: Math.max(1, laneEnds.length) };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export default function ClockTimeline({
|
|
|
|
|
- duration,
|
|
|
|
|
- rows,
|
|
|
|
|
- blocks,
|
|
|
|
|
- selectedBlockIds,
|
|
|
|
|
- onRowsChange,
|
|
|
|
|
- onBlocksChange,
|
|
|
|
|
- onSelectBlocks,
|
|
|
|
|
-}: Props) {
|
|
|
|
|
- const laneRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
|
|
|
- const [alignmentTick, setAlignmentTick] = useState<number | null>(null);
|
|
|
|
|
- const dragState = useRef<{
|
|
|
|
|
- blockId: string;
|
|
|
|
|
- mode: "move" | "resize";
|
|
|
|
|
- startX: number;
|
|
|
|
|
- laneWidth: number;
|
|
|
|
|
- shiftKey: boolean;
|
|
|
|
|
- wasSelected: boolean;
|
|
|
|
|
- groupOrigStarts: Map<string, number>; // blockId -> original start, for every block moving together
|
|
|
|
|
- origDuration: number;
|
|
|
|
|
- } | null>(null);
|
|
|
|
|
|
|
|
|
|
- const pxToTick = useCallback((laneWidth: number, px: number) => Math.round((px / laneWidth) * duration), [duration]);
|
|
|
|
|
-
|
|
|
|
|
- const addRow = () => {
|
|
|
|
|
- const row: ClockRow = {
|
|
|
|
|
- id: nextId("row"),
|
|
|
|
|
- name: `Row ${rows.length + 1}`,
|
|
|
|
|
- signals: [defaultActivationSignal],
|
|
|
|
|
- stackSize: 16,
|
|
|
|
|
- inserterCount: 1,
|
|
|
|
|
- };
|
|
|
|
|
- onRowsChange([...rows, row]);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const removeRow = (rowId: string) => {
|
|
|
|
|
- onRowsChange(rows.filter((r) => r.id !== rowId));
|
|
|
|
|
- const removedIds = new Set(blocks.filter((b) => b.rowId === rowId).map((b) => b.id));
|
|
|
|
|
- onBlocksChange(blocks.filter((b) => b.rowId !== rowId));
|
|
|
|
|
- if ([...selectedBlockIds].some((id) => removedIds.has(id))) {
|
|
|
|
|
- onSelectBlocks(new Set([...selectedBlockIds].filter((id) => !removedIds.has(id))));
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const updateRow = (rowId: string, patch: Partial<ClockRow>) => {
|
|
|
|
|
- onRowsChange(rows.map((r) => (r.id === rowId ? { ...r, ...patch } : r)));
|
|
|
|
|
- };
|
|
|
|
|
|
|
+export default function ClockTimeline({}: Props) {
|
|
|
|
|
+ const { duration, rowOrder, alignmentTick, addRow } = useClockStore();
|
|
|
|
|
|
|
|
- const updateRowSignal = (rowId: string, index: number, signal: Signal | null) => {
|
|
|
|
|
- if (!signal) return;
|
|
|
|
|
- const row = rows.find((r) => r.id === rowId);
|
|
|
|
|
- if (!row) return;
|
|
|
|
|
- const next = [...row.signals];
|
|
|
|
|
- next[index] = signal;
|
|
|
|
|
- updateRow(rowId, { signals: next });
|
|
|
|
|
- };
|
|
|
|
|
- const addRowSignal = (rowId: string, signal: Signal | null) => {
|
|
|
|
|
- if (!signal) return;
|
|
|
|
|
- const row = rows.find((r) => r.id === rowId);
|
|
|
|
|
- if (!row) return;
|
|
|
|
|
- updateRow(rowId, { signals: [...row.signals, signal] });
|
|
|
|
|
- };
|
|
|
|
|
- const removeRowSignal = (rowId: string, index: number) => {
|
|
|
|
|
- const row = rows.find((r) => r.id === rowId);
|
|
|
|
|
- if (!row || row.signals.length <= 1) return; // keep at least one
|
|
|
|
|
- updateRow(rowId, { signals: row.signals.filter((_, i) => i !== index) });
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const autoFillRow = (rowId: string, presetId: string) => {
|
|
|
|
|
- const row = rows.find((r) => r.id === rowId);
|
|
|
|
|
- if (!row) return;
|
|
|
|
|
- const preset = getPreset(presetId);
|
|
|
|
|
- const count = Math.max(1, row.inserterCount);
|
|
|
|
|
- const spacing = duration / count;
|
|
|
|
|
- const generated: ClockBlock[] = Array.from({ length: count }, (_, i) => ({
|
|
|
|
|
- id: nextId("block"),
|
|
|
|
|
- rowId,
|
|
|
|
|
- presetId,
|
|
|
|
|
- start: Math.round(i * spacing),
|
|
|
|
|
- duration: preset.ticks + 1,
|
|
|
|
|
- count: 1,
|
|
|
|
|
- }));
|
|
|
|
|
- onBlocksChange([...blocks.filter((b) => b.rowId !== rowId), ...generated]);
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ const majorTicks = useMemo(() => {
|
|
|
|
|
+ const step = Math.max(1, Math.round(duration / 10));
|
|
|
|
|
+ const out: number[] = [];
|
|
|
|
|
+ for (let t = 0; t <= duration; t += step) out.push(t);
|
|
|
|
|
+ return out;
|
|
|
|
|
+ }, [duration]);
|
|
|
|
|
|
|
|
const onPaletteDragStart = (e: React.DragEvent, presetId: string) => {
|
|
const onPaletteDragStart = (e: React.DragEvent, presetId: string) => {
|
|
|
e.dataTransfer.setData("text/plain", presetId);
|
|
e.dataTransfer.setData("text/plain", presetId);
|
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const onLaneDrop = (e: React.DragEvent, rowId: string) => {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- const presetId = e.dataTransfer.getData("text/plain");
|
|
|
|
|
- const preset = ACTION_PRESETS.find((p) => p.id === presetId);
|
|
|
|
|
- const lane = laneRefs.current[rowId];
|
|
|
|
|
- if (!preset || !lane) return;
|
|
|
|
|
- const rect = lane.getBoundingClientRect();
|
|
|
|
|
- const tick = Math.max(0, Math.min(duration - 1, pxToTick(rect.width, e.clientX - rect.left)));
|
|
|
|
|
- const block: ClockBlock = {
|
|
|
|
|
- id: nextId("block"),
|
|
|
|
|
- rowId,
|
|
|
|
|
- presetId: preset.id,
|
|
|
|
|
- start: tick,
|
|
|
|
|
- duration: preset.ticks + 1,
|
|
|
|
|
- count: 1,
|
|
|
|
|
- };
|
|
|
|
|
- onBlocksChange([...blocks, block]);
|
|
|
|
|
- onSelectBlocks(new Set([block.id]));
|
|
|
|
|
- };
|
|
|
|
|
- const onPointerDownBlock = (e: React.PointerEvent, block: ClockBlock, mode: "move" | "resize") => {
|
|
|
|
|
- e.stopPropagation();
|
|
|
|
|
- const lane = laneRefs.current[block.rowId];
|
|
|
|
|
- if (!lane) return;
|
|
|
|
|
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
|
|
|
-
|
|
|
|
|
- const wasSelected = selectedBlockIds.has(block.id);
|
|
|
|
|
- const groupIds = mode === "move" && wasSelected ? selectedBlockIds : new Set([block.id]);
|
|
|
|
|
-
|
|
|
|
|
- dragState.current = {
|
|
|
|
|
- blockId: block.id,
|
|
|
|
|
- mode,
|
|
|
|
|
- startX: e.clientX,
|
|
|
|
|
- laneWidth: lane.clientWidth,
|
|
|
|
|
- shiftKey: e.shiftKey,
|
|
|
|
|
- wasSelected,
|
|
|
|
|
- groupOrigStarts: new Map(blocks.filter((b) => groupIds.has(b.id)).map((b) => [b.id, b.start])),
|
|
|
|
|
- origDuration: block.duration,
|
|
|
|
|
- };
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const onPointerMove = (e: React.PointerEvent) => {
|
|
|
|
|
- const drag = dragState.current;
|
|
|
|
|
- if (!drag) return;
|
|
|
|
|
- const deltaTicks = pxToTick(drag.laneWidth, e.clientX - drag.startX);
|
|
|
|
|
-
|
|
|
|
|
- if (drag.mode === "resize") {
|
|
|
|
|
- updateBlockDuration(drag.blockId, Math.max(1, drag.origDuration + deltaTicks));
|
|
|
|
|
- setAlignmentTick(null);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Group move: apply the same delta to every block's original start,
|
|
|
|
|
- // clamped as a group so none of them fall off either edge.
|
|
|
|
|
- const patched = new Map<string, number>();
|
|
|
|
|
- let clampDelta = deltaTicks;
|
|
|
|
|
- drag.groupOrigStarts.forEach((origStart) => {
|
|
|
|
|
- const proposed = origStart + clampDelta;
|
|
|
|
|
- if (proposed < 0) clampDelta = Math.max(clampDelta, -origStart);
|
|
|
|
|
- if (proposed > duration - 1) clampDelta = Math.min(clampDelta, duration - 1 - origStart);
|
|
|
|
|
|
|
+ const handleAddRow = () => {
|
|
|
|
|
+ addRow({
|
|
|
|
|
+ id: `row-${Date.now()}`,
|
|
|
|
|
+ name: `Row ${rowOrder.length + 1}`,
|
|
|
|
|
+ signals: [defaultActivationSignal],
|
|
|
|
|
+ stackSize: 16,
|
|
|
|
|
+ inserterCount: 1,
|
|
|
});
|
|
});
|
|
|
- drag.groupOrigStarts.forEach((origStart, blockId) => patched.set(blockId, origStart + clampDelta));
|
|
|
|
|
-
|
|
|
|
|
- onBlocksChange(blocks.map((b) => (patched.has(b.id) ? { ...b, start: patched.get(b.id)! } : b)));
|
|
|
|
|
-
|
|
|
|
|
- // Alignment guide: does the primary dragged block now share a start tick
|
|
|
|
|
- // with any block outside the moving group?
|
|
|
|
|
- const draggedNewStart = patched.get(drag.blockId);
|
|
|
|
|
- const match = blocks.some((b) => !drag.groupOrigStarts.has(b.id) && b.start === draggedNewStart);
|
|
|
|
|
- setAlignmentTick(match && draggedNewStart !== undefined ? draggedNewStart : null);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const updateBlockDuration = (blockId: string, newDuration: number) => {
|
|
|
|
|
- onBlocksChange(blocks.map((b) => (b.id === blockId ? { ...b, duration: newDuration } : b)));
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const onPointerUp = (e: React.PointerEvent) => {
|
|
|
|
|
- const drag = dragState.current;
|
|
|
|
|
- dragState.current = null;
|
|
|
|
|
- setAlignmentTick(null);
|
|
|
|
|
- if (!drag) return;
|
|
|
|
|
-
|
|
|
|
|
- const moved = Math.abs(e.clientX - drag.startX) >= 3;
|
|
|
|
|
-
|
|
|
|
|
- if (drag.mode === "move" && !moved) {
|
|
|
|
|
- // Tap — resolve as a selection change.
|
|
|
|
|
- if (drag.shiftKey) {
|
|
|
|
|
- const next = new Set(selectedBlockIds);
|
|
|
|
|
- next.has(drag.blockId) ? next.delete(drag.blockId) : next.add(drag.blockId);
|
|
|
|
|
- onSelectBlocks(next);
|
|
|
|
|
- } else {
|
|
|
|
|
- onSelectBlocks(new Set([drag.blockId]));
|
|
|
|
|
- }
|
|
|
|
|
- } else if (drag.mode === "move" && moved && !drag.wasSelected) {
|
|
|
|
|
- // Actually dragged a block that wasn't part of the prior selection — it becomes the selection.
|
|
|
|
|
- onSelectBlocks(new Set([drag.blockId]));
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const majorTicks = useMemo(() => {
|
|
|
|
|
- const step = Math.max(1, Math.round(duration / 10));
|
|
|
|
|
- const out: number[] = [];
|
|
|
|
|
- for (let t = 0; t <= duration; t += step) out.push(t);
|
|
|
|
|
- return out;
|
|
|
|
|
- }, [duration]);
|
|
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className={styles.wrap}>
|
|
<div className={styles.wrap}>
|
|
|
<div className={styles.palette}>
|
|
<div className={styles.palette}>
|
|
|
{ACTION_PRESETS.map((p) => (
|
|
{ACTION_PRESETS.map((p) => (
|
|
|
- <div key={p.id} className={styles.chip} draggable onDragStart={(e) => onPaletteDragStart(e, p.id)}>
|
|
|
|
|
- {p.fromItem && <Icon iconName={`item/${p.fromItem}.png`} size={20} />}
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={p.id}
|
|
|
|
|
+ className={styles.chip}
|
|
|
|
|
+ draggable
|
|
|
|
|
+ onDragStart={(e) => onPaletteDragStart(e, p.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {p.fromItem && (
|
|
|
|
|
+ <Icon iconName={`item/${p.fromItem}.png`} size={20} />
|
|
|
|
|
+ )}
|
|
|
<span>{p.label}</span>
|
|
<span>{p.label}</span>
|
|
|
<span className={styles.chipTicks}>{p.ticks}t</span>
|
|
<span className={styles.chipTicks}>{p.ticks}t</span>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
- <span className={styles.hint}>Drag onto a row · Shift-click to multi-select · Ctrl+D to duplicate</span>
|
|
|
|
|
|
|
+ <span className={styles.hint}>
|
|
|
|
|
+ Drag onto a row · Shift-click to multi-select · Ctrl+D to duplicate
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className={styles.ruler}>
|
|
<div className={styles.ruler}>
|
|
|
{majorTicks.map((t) => (
|
|
{majorTicks.map((t) => (
|
|
|
- <div key={t} className={styles.rulerTick} style={{ left: `${(t / duration) * 100}%` }}>
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={t}
|
|
|
|
|
+ className={styles.rulerTick}
|
|
|
|
|
+ style={{ left: `${(t / duration) * 100}%` }}
|
|
|
|
|
+ >
|
|
|
<span>{t}</span>
|
|
<span>{t}</span>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
@@ -264,114 +77,17 @@ export default function ClockTimeline({
|
|
|
|
|
|
|
|
<div className={styles.rows}>
|
|
<div className={styles.rows}>
|
|
|
{alignmentTick !== null && (
|
|
{alignmentTick !== null && (
|
|
|
- <div className={styles.alignGuide} style={{ left: `${(alignmentTick / duration) * 100}%` }} />
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={styles.alignGuide}
|
|
|
|
|
+ style={{ left: `${(alignmentTick / duration) * 100}%` }}
|
|
|
|
|
+ />
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {rows.map((row) => {
|
|
|
|
|
- const rowBlocks = blocks.filter((b) => b.rowId === row.id);
|
|
|
|
|
- const allInstances = rowBlocks.flatMap((b) => expandBlockInstances(b));
|
|
|
|
|
- const { packed, laneCount } = packLanes(allInstances);
|
|
|
|
|
- const laneAreaHeight = laneCount * LANE_HEIGHT + (laneCount - 1) * LANE_GAP + 8;
|
|
|
|
|
|
|
+ {rowOrder.map((rowId) => (
|
|
|
|
|
+ <TimelineRow key={rowId} rowId={rowId} />
|
|
|
|
|
+ ))}
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <div key={row.id} className={styles.rowBlockWrap}>
|
|
|
|
|
- <div className={styles.rowHeader}>
|
|
|
|
|
- <input
|
|
|
|
|
- className={styles.rowNameInput}
|
|
|
|
|
- value={row.name}
|
|
|
|
|
- onChange={(e) => updateRow(row.id, { name: e.target.value })}
|
|
|
|
|
- />
|
|
|
|
|
- <div className={styles.signalList}>
|
|
|
|
|
- {row.signals.map((s, i) => (
|
|
|
|
|
- <div key={`${s.name}-${i}`} className={styles.signalChip}>
|
|
|
|
|
- <SelectSignal value={s.name} onSelectSignal={(_, sig) => updateRowSignal(row.id, i, sig)} />
|
|
|
|
|
- {row.signals.length > 1 && (
|
|
|
|
|
- <button className={styles.signalRemove} onClick={() => removeRowSignal(row.id, i)}>
|
|
|
|
|
- ✕
|
|
|
|
|
- </button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- <SelectSignal key={row.signals.length} onSelectSignal={(_, sig) => addRowSignal(row.id, sig)} />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className={styles.rowStats}>
|
|
|
|
|
- <label>Stack</label>
|
|
|
|
|
- <ExpressionInput
|
|
|
|
|
- value={row.stackSize}
|
|
|
|
|
- min={1}
|
|
|
|
|
- onCommit={(v) => updateRow(row.id, { stackSize: v })}
|
|
|
|
|
- />
|
|
|
|
|
- <label>Inserters</label>
|
|
|
|
|
- <ExpressionInput
|
|
|
|
|
- value={row.inserterCount}
|
|
|
|
|
- min={1}
|
|
|
|
|
- onCommit={(v) => updateRow(row.id, { inserterCount: v })}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <select className={styles.autoFillPreset} defaultValue="chest_to_belt" id={`autofill-preset-${row.id}`}>
|
|
|
|
|
- {ACTION_PRESETS.filter((p) => p.id !== "custom").map((p) => (
|
|
|
|
|
- <option key={p.id} value={p.id}>
|
|
|
|
|
- {p.label}
|
|
|
|
|
- </option>
|
|
|
|
|
- ))}
|
|
|
|
|
- </select>
|
|
|
|
|
- <button
|
|
|
|
|
- className={styles.autoFillBtn}
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- const select = document.getElementById(`autofill-preset-${row.id}`) as HTMLSelectElement | null;
|
|
|
|
|
- autoFillRow(row.id, select?.value ?? "chest_to_belt");
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- Auto-fill ({row.inserterCount})
|
|
|
|
|
- </button>
|
|
|
|
|
- <button className={styles.removeBtn} onClick={() => removeRow(row.id)}>
|
|
|
|
|
- ✕
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div
|
|
|
|
|
- className={styles.lane}
|
|
|
|
|
- style={{ height: laneAreaHeight }}
|
|
|
|
|
- ref={(el) => (laneRefs.current[row.id] = el)}
|
|
|
|
|
- onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
- onDrop={(e) => onLaneDrop(e, row.id)}
|
|
|
|
|
- onPointerMove={onPointerMove}
|
|
|
|
|
- onPointerUp={onPointerUp}
|
|
|
|
|
- >
|
|
|
|
|
- {packed.map((inst) => {
|
|
|
|
|
- const block = rowBlocks.find((b) => b.id === inst.blockId)!;
|
|
|
|
|
- const preset = getPreset(block.presetId);
|
|
|
|
|
- const left = (inst.start / duration) * 100;
|
|
|
|
|
- const width = (inst.duration / duration) * 100;
|
|
|
|
|
- const selected = selectedBlockIds.has(block.id);
|
|
|
|
|
- const aligned = alignmentTick !== null && inst.start === alignmentTick;
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- key={inst.id}
|
|
|
|
|
- className={`${styles.block} ${selected ? styles.blockSelected : ""} ${aligned ? styles.blockAligned : ""}`}
|
|
|
|
|
- style={{
|
|
|
|
|
- left: `${left}%`,
|
|
|
|
|
- width: `${width}%`,
|
|
|
|
|
- top: inst.lane * (LANE_HEIGHT + LANE_GAP) + 4,
|
|
|
|
|
- height: LANE_HEIGHT,
|
|
|
|
|
- }}
|
|
|
|
|
- onPointerDown={(e) => onPointerDownBlock(e, block, "move")}
|
|
|
|
|
- >
|
|
|
|
|
- <div className={styles.blockIcons}>
|
|
|
|
|
- {preset.fromItem && <Icon iconName={`item/${preset.fromItem}.png`} size={18} />}
|
|
|
|
|
- {preset.toItem && <Icon iconName={`item/${preset.toItem}.png`} size={18} />}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div
|
|
|
|
|
- className={styles.resizeHandle}
|
|
|
|
|
- onPointerDown={(e) => onPointerDownBlock(e, block, "resize")}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- <button className={styles.addRowBtn} onClick={addRow}>
|
|
|
|
|
|
|
+ <button className={styles.addRowBtn} onClick={handleAddRow}>
|
|
|
+ Add row
|
|
+ Add row
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|