]> git.localhorst.tv Git - alttp.git/commitdiff
zootr draw connectors
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 25 May 2025 12:02:32 +0000 (14:02 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 25 May 2025 12:02:32 +0000 (14:02 +0200)
resources/js/components/zootr/MixedPoolsTracker.js
resources/sass/zootr.scss

index 3a62bdb738a858ab4209891587e70bc2ed5f3b58..f27b547c627540e8b7be6ae165ce606e8bedc0c7 100644 (file)
@@ -2411,6 +2411,41 @@ const vecAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
 
 const vecMul = (v, f) => ({ x: v.x * f, y: v.y * f });
 
+const MAPS = AREAS
+       .filter((area) => !!area.map)
+       .map((area) => ({
+               id: area.id,
+               name: area.name,
+               pos: area.map.pos,
+               size: area.map.size,
+               bg: {
+                       src: area.map.bg.src,
+                       pos: vecAdd(area.map.pos, area.map.bg.off),
+                       size: vecMul(area.map.size, area.map.bg.scale),
+               },
+               entrances: area.entrances
+                       .filter((entrance) => entrance.pos)
+                       .map((entrance) => ({
+                               id: `${area.id}.${entrance.id}`,
+                               name: entranceFull({ ...entrance, area }),
+                               pos: vecAdd(area.map.pos, entrance.pos),
+                               color: entrance.bgColor,
+                       })),
+       }));
+
+const getMapEntrance = (id) => {
+       if (!id) return null;
+       const dotPos = id.indexOf('.');
+       if (dotPos === -1) {
+               return null;
+       }
+       const areaId = id.substring(0, dotPos);
+       const area = MAPS.find((a) => a.id === areaId);
+       if (!area) return null;
+       const entrance = area.entrances.find((e) => e.id === id);
+       return entrance;
+};
+
 const SelectBox = ({ id, name, onChange, options, value }) => {
        const [open, setOpen] = React.useState(false);
        const [search, setSearch] = React.useState('');
@@ -2580,21 +2615,37 @@ EntranceRow.propTypes = {
 };
 
 const MapEntrance = ({ entrance }) => {
-       const { connections, setConnection } = useTracker();
+       const {
+               connections,
+               isDragging,
+               onMapEntranceClick,
+               setConnection,
+       } = useTracker();
+
+       const classNames = ['entrance'];
+       if (connections[entrance.id] === 'trash') classNames.push('is-trash');
+       if (isDragging(entrance)) classNames.push('is-dragging');
 
        return <circle
                cx={entrance.pos.x}
                cy={entrance.pos.y}
                r="3"
-               className={`entrance${connections[entrance.id] === 'trash' ? ' is-trash' : ''}`}
+               className={classNames.join(' ')}
                fill={entrance.color}
                stroke="#000000"
-               onClick={() => {
-                       if (connections[entrance.id] === 'trash') {
+               onClick={(e) => {
+                       onMapEntranceClick(entrance);
+                       e.preventDefault();
+                       e.stopPropagation();
+               }}
+               onContextMenu={(e) => {
+                       if (connections[entrance.id]) {
                                setConnection(entrance.id, null);
                        } else {
                                setConnection(entrance.id, 'trash');
                        }
+                       e.preventDefault();
+                       e.stopPropagation();
                }}
        >
                <title>{entrance.name}</title>
@@ -2610,11 +2661,37 @@ MapEntrance.propTypes = {
                        x: PropTypes.number,
                        y: PropTypes.number,
                })
+       }),
+};
+
+const MapConnector = ({ from, to }) => {
+       return <line
+               className="connector"
+               x1={from.pos.x}
+               y1={from.pos.y}
+               x2={to.pos.x}
+               y2={to.pos.y}
+       />;
+};
+
+MapConnector.propTypes = {
+       from: PropTypes.shape({
+               pos: PropTypes.shape({
+                       x: PropTypes.number,
+                       y: PropTypes.number,
+               })
+       }),
+       to: PropTypes.shape({
+               pos: PropTypes.shape({
+                       x: PropTypes.number,
+                       y: PropTypes.number,
+               })
        })
 };
 
 const MixedPoolsTracker = () => {
        const [connections, setConnections] = React.useState({});
+       const [dragging, setDragging] = React.useState(null);
 
        const setConnection = React.useCallback((src, dst) => {
                setConnections((c) => {
@@ -2651,9 +2728,34 @@ const MixedPoolsTracker = () => {
                return options;
        }, []);
 
+       const isDragging = React.useCallback((entrance) => {
+               return dragging === entrance.id;
+       }, [dragging]);
+
+       const onMapEntranceClick = React.useCallback((entrance) => {
+               if (dragging) {
+                       if (dragging !== entrance.id) {
+                               setConnection(dragging, entrance.id);
+                       }
+                       setDragging(null);
+               } else {
+                       setDragging(entrance.id);
+               }
+       }, [dragging, setConnection, setDragging]);
+
        const context = React.useMemo(() => ({
-               connections, entrances, setConnection,
-       }), [connections, entrances, setConnection]);
+               connections,
+               entrances,
+               isDragging,
+               onMapEntranceClick,
+               setConnection,
+       }), [
+               connections,
+               entrances,
+               isDragging,
+               onMapEntranceClick,
+               setConnection,
+       ]);
 
        const superGroups = React.useMemo(() => {
                const sg = [
@@ -2696,55 +2798,26 @@ const MixedPoolsTracker = () => {
                return sg;
        }, [connections]);
 
-       const maps = React.useMemo(() => {
-               return AREAS
-                       .filter((area) => !!area.map)
-                       .map((area) => ({
-                               id: area.id,
-                               pos: area.map.pos,
-                               size: area.map.size,
-                               bg: {
-                                       src: area.map.bg.src,
-                                       pos: vecAdd(area.map.pos, area.map.bg.off),
-                                       size: vecMul(area.map.size, area.map.bg.scale),
-                               },
-                               entrances: area.entrances
-                                       .filter((entrance) => entrance.pos)
-                                       .map((entrance) => ({
-                                               id: `${area.id}.${entrance.id}`,
-                                               name: entranceFull({ ...entrance, area }),
-                                               pos: vecAdd(area.map.pos, entrance.pos),
-                                               color: entrance.bgColor,
-                                       })),
-                       }));
-       }, []);
-
        const connectors = React.useMemo(() => {
                const cs = [];
                Object.entries(connections).forEach(([from, to]) => {
+                       const fromEntrance = getEntrance(from);
+                       if (!fromEntrance) return;
+                       if (from > to && !fromEntrance.oneway) return;
+                       const fromMap = getMapEntrance(from);
+                       if (!fromMap) return;
+                       const toMap = getMapEntrance(to);
+                       if (!toMap) return;
+                       cs.push({
+                               from: fromMap,
+                               to: toMap,
+                       });
                });
                return cs;
-       }, [connections, maps]);
+       }, [connections]);
 
        return <CONTEXT.Provider value={context}>
                <div className="mixed-pools-tracker">
-                       <div className="map">
-                               <svg viewBox="0 0 500 370">
-                                       {maps.map((map) =>
-                                               <g key={map.id} title={map.name}>
-                                                       <image
-                                                               href={map.bg.src}
-                                                               pointerEvents="none"
-                                                               x={map.bg.pos.x} y={map.bg.pos.y}
-                                                               width={map.bg.size.x}
-                                                       />
-                                                       {map.entrances.map((entrance) =>
-                                                               <MapEntrance key={entrance.id} entrance={entrance} />
-                                                       )}
-                                               </g>
-                                       )}
-                               </svg>
-                       </div>
                        <div className="columns">
                                {superGroups.map((sg) =>
                                        <div className="column" key={sg.key}>
@@ -2801,6 +2874,36 @@ const MixedPoolsTracker = () => {
                                        )}
                                </div>
                        </div>
+                       <div className="map mt-5">
+                               <svg
+                                       viewBox="0 0 500 370"
+                                       onClick={() => { setDragging(null); }}
+                                       onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); }}
+                               >
+                                       {MAPS.map((map) =>
+                                               <g key={map.id} title={map.name}>
+                                                       <image
+                                                               href={map.bg.src}
+                                                               pointerEvents="none"
+                                                               x={map.bg.pos.x} y={map.bg.pos.y}
+                                                               width={map.bg.size.x}
+                                                       />
+                                                       {map.entrances.map((entrance) =>
+                                                               <MapEntrance key={entrance.id} entrance={entrance} />
+                                                       )}
+                                               </g>
+                                       )}
+                                       <g title="connectors">
+                                               {connectors.map((c) =>
+                                                       <MapConnector
+                                                               key={`${c.from.id}-${c.to.id}`}
+                                                               from={c.from}
+                                                               to={c.to}
+                                                       />
+                                               )}
+                                       </g>
+                               </svg>
+                       </div>
                </div>
        </CONTEXT.Provider>;
 };
index 2ccd13820fe3810a16b1d2b4ca0dc8109042cca0..3070837c7d855ccd26d44083430b358e8b3e4d47 100644 (file)
                cursor: pointer;
        }
        .map {
+               svg {
+                       max-width: 104em;
+               }
+               .connector {
+                       stroke: #cc0000;
+                       pointer-events: none;
+               }
                .entrance {
                        &.is-trash {
                                fill: #000000;
                                stroke: #ffffff;
                                filter: drop-shadow(0 0 1px #000000) drop-shadow(0 0 2px #000000);
                        }
+                       &.is-dragging {
+                               stroke: #cc0000;
+                               filter: drop-shadow(0 0 1px #cc0000) drop-shadow(0 0 2px #cc0000);
+                       }
                }
        }
 }