Explorar el Código

fix icon and make signals reorder i timeline row

clovis hace 1 mes
padre
commit
0bf6215ac2

+ 3 - 0
index.html

@@ -10,6 +10,9 @@
       href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap"
       rel="stylesheet"
     />
+    <link rel="preload" href="/data/2.0/icon_56.webp" as="image" type="image/webp" />
+    <link rel="preload" href="/data/2.0/icon_64.webp" as="image" type="image/webp" />
+    <link rel="preload" href="/data/2.0/icon_128.webp" as="image" type="image/webp" />
     <title>factorio-clockmaster</title>
   </head>
   <body>

+ 13 - 21
src/assets/ClockTimeline/ClockTimeline.module.css

@@ -96,17 +96,6 @@
   outline: 2px solid #f1be64;
   outline-offset: 1px;
 }
-.removeBtn {
-  margin-left: auto;
-  background: none;
-  border: none;
-  color: #999;
-  cursor: pointer;
-}
-.removeBtn:hover {
-  color: #d9614f;
-}
-
 .lane {
   position: relative;
   background-color: #1a1a1a;
@@ -210,23 +199,26 @@
 .signalList {
   display: flex;
   align-items: center;
-  gap: 4px;
+  gap: 10px;
 }
 .signalChip {
   display: flex;
   align-items: center;
-  gap: 2px;
+  gap: 4px;
+  position: relative;
 }
-.signalRemove {
-  background: none;
-  border: none;
-  color: #999;
+
+.signalRemove,
+.removeBtn {
   font-size: 10px;
-  cursor: pointer;
+  height: 32px;
+  min-width: 10px;
+  padding: 8px 3px;
+  margin-left: 0px;
+  margin-right: 0;
 }
-
-.signalRemove:hover {
-  color: #d9614f;
+.removeBtn {
+  margin-left: auto;
 }
 
 .autoFillPreset {

+ 0 - 1
src/assets/ClockTimeline/ClockTimeline.tsx

@@ -24,7 +24,6 @@ export default function ClockTimeline({}: Props) {
     for (let t = 0; t <= duration; t += step) out.push(t);
     return out;
   }, [duration]);
-
   const onPaletteDragStart = (e: React.DragEvent, presetId: string) => {
     e.dataTransfer.setData("text/plain", presetId);
     e.dataTransfer.effectAllowed = "copy";

+ 56 - 0
src/assets/ClockTimeline/SignalEditorList.tsx

@@ -0,0 +1,56 @@
+import { useRef } from "react";
+import type { Signal } from "../types";
+
+import styles from "./ClockTimeline.module.css";
+import SelectSignal from "../Selector/SelectSignal";
+
+interface SignalEditorListProps {
+  signals: Signal[];
+  onAdd: (signal: Signal | null) => void;
+  onUpdate: (index: number, signal: Signal | null) => void;
+  onRemove: (index: number) => void;
+  onReorder: (sourceIdx: number, targetIdx: number) => void;
+}
+
+export default function SignalEditorList({ signals, onAdd, onUpdate, onRemove, onReorder }: SignalEditorListProps) {
+  const draggedSignalIdx = useRef<number | null>(null);
+
+  return (
+    <div className={styles.signalList}>
+      {signals.map((s, i) => (
+        <div
+          key={`${s.name}-${i}`}
+          className={styles.signalChip}
+          draggable={signals.length > 1}
+          onDragStart={(e) => {
+            draggedSignalIdx.current = i;
+            e.dataTransfer.effectAllowed = "move";
+          }}
+          onDragOver={(e) => {
+            e.preventDefault();
+            e.dataTransfer.dropEffect = "move";
+          }}
+          onDrop={(e) => {
+            e.preventDefault();
+            const sourceIdx = draggedSignalIdx.current;
+            if (sourceIdx !== null && sourceIdx !== i) {
+              onReorder(sourceIdx, i);
+            }
+            draggedSignalIdx.current = null;
+          }}
+          onDragEnd={() => {
+            draggedSignalIdx.current = null;
+          }}
+        >
+          <SelectSignal value={s.name} onSelectSignal={(_, sig) => onUpdate(i, sig)} />
+          {signals.length > 1 && (
+            <button className={"button " + styles.signalRemove} onClick={() => onRemove(i)}>
+              ✕
+            </button>
+          )}
+        </div>
+      ))}
+      <SelectSignal className={styles.signalEmpty} key={signals.length} onSelectSignal={(_, sig) => onAdd(sig)} />
+    </div>
+  );
+}

+ 19 - 18
src/assets/ClockTimeline/TimelineRow.tsx

@@ -6,6 +6,7 @@ import ExpressionInput from "../components/ExpressionInpux";
 import Icon from "../icon";
 import styles from "./ClockTimeline.module.css";
 import { ACTION_PRESETS, expandBlockInstances, getPreset, type ClockBlock, type Signal } from "../types";
+import SignalEditorList from "./SignalEditorList";
 
 type PackedInstance = {
   id: string;
@@ -42,6 +43,7 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
   const { onPointerDownBlock, onPointerMove, onPointerUp, pxToTick } = useTimelineDrag();
 
   const laneRef = useRef<HTMLDivElement>(null);
+
   const row = rows[rowId];
   if (!row) return null;
 
@@ -76,17 +78,26 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
     if (!signal) return;
     updateRow(rowId, { signals: [...row.signals, signal] });
   };
+
   const updateRowSignal = (index: number, signal: Signal | null) => {
     if (!signal) return;
     const next = [...row.signals];
     next[index] = signal;
     updateRow(rowId, { signals: next });
   };
+
   const removeRowSignal = (index: number) => {
     if (!row || row.signals.length <= 1) return;
     updateRow(rowId, { signals: row.signals.filter((_, i) => i !== index) });
   };
 
+  const reorderRowSignals = (sourceIdx: number, targetIdx: number) => {
+    const nextSignals = [...row.signals];
+    const [movedSignal] = nextSignals.splice(sourceIdx, 1);
+    nextSignals.splice(targetIdx, 0, movedSignal);
+    updateRow(rowId, { signals: nextSignals });
+  };
+
   const autoFillRow = (presetId: string) => {
     if (!row) return;
     const preset = getPreset(presetId);
@@ -111,23 +122,13 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
           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(i, sig)} />
-              {row.signals.length > 1 && (
-                <button className={styles.signalRemove} onClick={() => removeRowSignal(i)}>
-                  ✕
-                </button>
-              )}
-            </div>
-          ))}
-          <SelectSignal
-            className={styles.signalEmpty}
-            key={row.signals.length}
-            onSelectSignal={(_, sig) => addRowSignal(sig)}
-          />
-        </div>
+        <SignalEditorList
+          signals={row.signals}
+          onAdd={addRowSignal}
+          onUpdate={updateRowSignal}
+          onRemove={removeRowSignal}
+          onReorder={reorderRowSignals}
+        />
         <div className={styles.rowStats}>
           <label>Stack</label>
           <ExpressionInput value={row.stackSize} min={1} onCommit={(v) => updateRow(rowId, { stackSize: v })} />
@@ -150,7 +151,7 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
         >
           Auto-fill ({row.inserterCount})
         </button>
-        <button className={styles.removeBtn} onClick={() => removeRow(row.id)}>
+        <button className={"button " + styles.removeBtn} onClick={() => removeRow(row.id)}>
         </button>
       </div>

+ 10 - 0
src/assets/Icon.module.css

@@ -1,9 +1,19 @@
 .factorio-icon {
   display: inline-block;
   position: relative;
+  background-clip: border-box;
+  background-origin: border-box;
+  background-repeat: repeat;
+  overflow: hidden;
 }
 .factorio-icon .factorio-icon {
   position: absolute;
   left: 0;
   bottom: 0;
 }
+.factorio-icon img {
+  position: absolute;
+  max-width: none;
+  pointer-events: none;
+  user-select: none;
+}

+ 2 - 3
src/assets/Selector/MachineSelector.tsx

@@ -94,13 +94,12 @@ function MachineSelector({ style, className, onChange }: MachineSelectorProps) {
         renderInput={(params: any) => (
           <TextField
             {...params}
-            label={recipe ? "Machine (Shift+Scroll)" : "Select recipe first"}
+            label={recipe ? "Machine (Alt+Scroll)" : "Select recipe first"}
             InputProps={{
               ...params.InputProps,
               startAdornment: machine ? (
                 <InputAdornment position="start">
-                  {/* Just attach the scrollRef here! */}
-                  <div title="Shift + Scroll to change quality" style={{ cursor: "ns-resize", display: "flex" }}>
+                  <div title="Alt + Scroll to change quality" style={{ cursor: "ns-resize", display: "flex" }}>
                     <Icon iconName={machine.icon ?? ""} size={28} qualityLevel={activeQuality.level} />
                   </div>
                 </InputAdornment>

+ 5 - 2
src/assets/Selector/SelectFactorioMenu.module.css

@@ -4,8 +4,11 @@
   flex-direction: column;
   gap: 8px;
   padding: 8px;
-  background: #646464;
-  background-color: #313031;
+  background: #313031;
+  box-shadow:
+    0px 0px 1px 1px rgba(0, 0, 0, 0.2),
+    0px 1px 1px 0px rgba(0, 0, 0, 0.5),
+    0px 1px 3px 0px rgba(0, 0, 0, 0.5);
 }
 
 .select-menu-header {

+ 12 - 4
src/assets/Selector/SelectFactorioMenu.tsx

@@ -27,6 +27,7 @@ export type SubGroup<T> = {
 export type MenuItem = { name: string; icon: string };
 
 interface SubGroupProps {
+  activeQualityLevel?: number;
   subgroup: SubGroup<MenuItem>;
   selectedItem: string;
   onSelectItem: (name: string) => void;
@@ -71,7 +72,7 @@ function filterCategory(category: Category<MenuItem>, search: string): Category<
  * @component
  * @param {SubGroupProps} props
  */
-function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
+function SubGroupRow({ subgroup, activeQualityLevel, selectedItem, onSelectItem }: SubGroupProps) {
   const emptySlotsCounts = useMemo(() => {
     const lastRowSlots = subgroup.children.length % ITEM_PER_ROW;
     return lastRowSlots > 0 ? ITEM_PER_ROW - lastRowSlots : 0;
@@ -84,7 +85,7 @@ function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
             className={`${styles.selectMenuIcon} ${o.name === selectedItem ? styles.active : ""}`}
             onClick={() => onSelectItem(o.name)}
           >
-            <Icon iconName={o.icon} size={30} />
+            <Icon iconName={o.icon} size={30} qualityLevel={activeQualityLevel} />
           </div>
         </Tooltip>
       ))}
@@ -104,6 +105,7 @@ type SelectMenuProps = {
   style?: CSSProperties;
   categories: Category<MenuItem>[];
   title?: string;
+  showQuality?: boolean;
   onClose?: () => void;
   onSelectItem?: (itemName: string, qualityLevel: number) => void;
 };
@@ -121,7 +123,7 @@ type SelectMenuProps = {
  * @component
  * @param {SelectMenuProps} props
  */
-function SelectMenu({ style, className, title, categories, onClose, onSelectItem }: SelectMenuProps) {
+function SelectMenu({ style, className, title, categories, showQuality, onClose, onSelectItem }: SelectMenuProps) {
   const [category, setCategory] = useState(categories[0].name);
   const [item, selectItem] = useState("");
   const [showSearch, setShowSearch] = useState(false);
@@ -173,7 +175,13 @@ function SelectMenu({ style, className, title, categories, onClose, onSelectItem
       <div className={styles.selectMenuContent}>
         <SimpleBar style={{ maxHeight: 300 }}>
           {filteredContent?.subGroup.map((subgroup) => (
-            <SubGroupRow key={subgroup.name} subgroup={subgroup} selectedItem={item} onSelectItem={handleItemSelect} />
+            <SubGroupRow
+              key={subgroup.name}
+              subgroup={subgroup}
+              selectedItem={item}
+              onSelectItem={handleItemSelect}
+              activeQualityLevel={showQuality ? activeQuality.level : 0}
+            />
           ))}
         </SimpleBar>
       </div>

+ 3 - 2
src/assets/Selector/SelectSignal.tsx

@@ -32,8 +32,9 @@ function SelectSignal({ style, className, value, placeholder, onSelectSignal }:
     [anchorEl, showMenu],
   );
   const onSelectRecipe = useCallback(
-    (name: string) => {
-      const found = signals.find((o) => o.name === name) ?? null;
+    (name: string, qualityLevel: number) => {
+      const rawSignal = signals.find((o) => o.name === name);
+      const found = rawSignal ? { ...rawSignal, quality: qualityLevel } : null;
       onSelectSignal && onSelectSignal(name, found);
       setInternalSignal(found);
       setAnchorEl(null);

+ 14 - 9
src/assets/components/QualityIcon.tsx

@@ -1,10 +1,10 @@
-import  { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef } from "react";
 
 import styles from "./QualityIcon.module.css";
 import data from "../../assets/data/2.0/data.json";
 import Icon from "../icon";
 
-const QUALITY_LEVELS = data.qualityLevels.map(q => q.name); // ["normal", "uncommon", "rare", "epic", "legendary"]
+const QUALITY_LEVELS = data.qualityLevels.map((q) => q.name); // ["normal", "uncommon", "rare", "epic", "legendary"]
 
 type QualityIconProps = {
   iconName: string;
@@ -13,7 +13,12 @@ type QualityIconProps = {
   onChangeQuality?: (newQuality: string) => void;
 };
 
-export default function QualityIcon({ iconName, size = 40, initialQuality = "normal", onChangeQuality }: QualityIconProps) {
+export default function QualityIcon({
+  iconName,
+  size = 40,
+  initialQuality = "normal",
+  onChangeQuality,
+}: QualityIconProps) {
   const [quality, setQuality] = useState(initialQuality);
   const containerRef = useRef<HTMLDivElement>(null);
 
@@ -21,13 +26,13 @@ export default function QualityIcon({ iconName, size = 40, initialQuality = "nor
     const handleWheel = (e: WheelEvent) => {
       if (!e.shiftKey) return;
       e.preventDefault(); // Prevent page scroll
-      
+
       const currentIndex = QUALITY_LEVELS.indexOf(quality);
       const direction = Math.sign(e.deltaY); // 1 for down, -1 for up
-      
+
       // Shift+Scroll Up = Higher quality, Down = Lower quality
       const nextIndex = Math.max(0, Math.min(QUALITY_LEVELS.length - 1, currentIndex - direction));
-      
+
       if (nextIndex !== currentIndex) {
         const newQuality = QUALITY_LEVELS[nextIndex];
         setQuality(newQuality);
@@ -43,11 +48,11 @@ export default function QualityIcon({ iconName, size = 40, initialQuality = "nor
     }
   }, [quality, onChangeQuality]);
 
-  const qualityData = data.qualityLevels.find(q => q.name === quality);
+  const qualityData = data.qualityLevels.find((q) => q.name === quality);
 
   return (
-    <div ref={containerRef} className={styles.wrap} title="Shift + Scroll to change quality">
+    <div ref={containerRef} className={styles.wrap} title="Alt + Scroll to change quality">
       <Icon iconName={iconName} size={size} qualityLevel={qualityData?.level} />
     </div>
   );
-}
+}

+ 26 - 19
src/assets/icon.tsx

@@ -18,32 +18,39 @@ const iconCountPerSize = iconMap.reduce((acc, icon) => {
   return acc;
 }, new Map<number, number>());
 
-const mapSizePerSize = new Map<number, number>();
+const mapSizePerSize = new Map<number, number[]>();
 for (let [size, count] of iconCountPerSize.entries()) {
-  mapSizePerSize.set(size, Math.floor(Math.sqrt(count)) + 1);
+  const cols = Math.ceil(Math.sqrt(count));
+  const rows = Math.ceil(count / cols);
+
+  mapSizePerSize.set(size, [cols, rows]);
 }
 
 function Icon({ style, className, iconName, size = 64, qualityLevel = 0 }: IconProps) {
   const icon = useMemo(() => iconMap.find((o) => o.name == iconName), [iconName]);
-  const iconStyle = useMemo(() => {
-    if (icon) {
-      const bgSizePx = (mapSizePerSize.get(icon.size) ?? 1) * size;
-      const offsetX = (-(icon.x ?? 0) * size) / (icon.size ?? 1);
-      const offsetY = (-(icon.y ?? 0) * size) / (icon.size ?? 1);
-      return {
-        backgroundImage: `url("/data/2.0/icon_${icon?.size}.webp")`,
-        backgroundPosition: `${offsetX}px ${offsetY}px`,
-        backgroundSize: `${bgSizePx}px`,
-        width: size,
-        height: size,
-        ...style,
-      };
-    }
-  }, [icon, size, style]);
+  if (!icon) return null;
+
+  const shape = mapSizePerSize.get(icon.size) ?? [];
+  const bgWidthPx = (shape[0] ?? 1) * size;
+  const bgHeightPx = (shape[1] ?? 1) * size;
+
+  const offsetX = (-(icon.x ?? 0) * size) / (icon.size ?? 1);
+  const offsetY = (-(icon.y ?? 0) * size) / (icon.size ?? 1);
   const qualityIcon = useMemo(() => qualityLevels.find((q) => q.level == qualityLevel)?.icon, [qualityLevel]);
   return (
-    <i className={styles.factorioIcon + " " + className} style={iconStyle}>
-      {qualityLevel > 0 && qualityIcon != undefined && <Icon iconName={qualityIcon} size={size / 2}></Icon>}
+    <i className={styles.factorioIcon + " " + (className ?? "")} style={{ width: size, height: size, ...styles }}>
+      <img
+        src={`/data/2.0/icon_${icon.size}.webp`}
+        alt={iconName}
+        draggable={false}
+        style={{
+          width: bgWidthPx,
+          height: bgHeightPx,
+          left: offsetX,
+          top: offsetY,
+        }}
+      />
+      {qualityLevel > 0 && qualityIcon != undefined && <Icon iconName={qualityIcon} size={size / 3}></Icon>}
     </i>
   );
 }

+ 1 - 1
src/assets/types.ts

@@ -26,7 +26,7 @@ export type Signal = {
   subgroup: string;
   icon: string;
   order: string;
-  quality?: string;
+  quality?: number;
 };
 
 /** A single activation Clock, placed freely on the timeline. */

+ 7 - 11
src/hooks/useQualityScroller.ts

@@ -20,12 +20,8 @@ export type Quality = {
 };
 
 //  Precompute the exact sequence by traversing the 'next' linked list
-const qualityMap = new Map<string, Quality>(
-  data.qualityLevels.map((q: Quality) => [q.name, q]),
-);
-const baseQuality =
-  data.qualityLevels.find((q: Quality) => q.level === 0) ||
-  data.qualityLevels[0];
+const qualityMap = new Map<string, Quality>(data.qualityLevels.map((q: Quality) => [q.name, q]));
+const baseQuality = data.qualityLevels.find((q: Quality) => q.level === 0) || data.qualityLevels[0];
 
 export const orderedQualities: Quality[] = [];
 let current: Quality | undefined = baseQuality;
@@ -38,7 +34,7 @@ while (current) {
 export function useQualityScroller(
   initialQualityName: string = baseQuality.name,
   onChange?: (quality: Quality) => void,
-  persistKey?: string 
+  persistKey?: string,
 ) {
   const [qualityName, setQualityName] = useState(() => {
     if (persistKey) {
@@ -70,14 +66,14 @@ export function useQualityScroller(
     if (!el) return;
 
     const handleWheel = (e: WheelEvent) => {
-      if (!e.shiftKey) return;
-      e.preventDefault(); 
+      if (!e.altKey) return;
+      e.preventDefault();
 
       setQualityName((prevName) => {
         const currentIndex = orderedQualities.findIndex((q) => q.name === prevName);
         if (currentIndex === -1) return prevName;
 
-        const direction = -Math.sign(e.deltaY);
+        const direction = Math.sign(e.deltaY);
         let nextIndex = currentIndex - direction;
         nextIndex = Math.max(0, Math.min(orderedQualities.length - 1, nextIndex));
 
@@ -96,7 +92,7 @@ export function useQualityScroller(
 
   const activeQuality = useMemo(
     () => orderedQualities.find((q) => q.name === qualityName) || baseQuality,
-    [qualityName]
+    [qualityName],
   );
 
   return { scrollRef, activeQuality, setQualityName };