velxio/frontend/src/components/simulator/WireLayer.tsx

104 lines
3.3 KiB
TypeScript

import React from 'react';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { WireRenderer } from './WireRenderer';
import { WireInProgressRenderer } from './WireInProgressRenderer';
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
export interface SegmentHandle {
segIndex: number;
axis: 'horizontal' | 'vertical';
mx: number; // midpoint X
my: number; // midpoint Y
}
interface WireLayerProps {
hoveredWireId: string | null;
/** Segment drag preview: overrides the path of a specific wire */
segmentDragPreview: { wireId: string; overridePath: string } | null;
/** Handles to render for the selected wire */
segmentHandles: SegmentHandle[];
/** Called when user starts dragging a handle (passes segIndex) */
onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void;
/** Called when user starts dragging a handle via touch (passes segIndex) */
onHandleTouchStart?: (e: React.TouchEvent, segIndex: number) => void;
/** Whether the user is in touch-aiming mode (shows crosshair on wire preview) */
isAiming?: boolean;
/** Current canvas zoom level — handles scale inversely to stay constant on screen */
zoom?: number;
}
export const WireLayer: React.FC<WireLayerProps> = ({
hoveredWireId,
segmentDragPreview,
segmentHandles,
onHandleMouseDown,
onHandleTouchStart,
isAiming,
zoom = 1,
}) => {
const wires = useSimulatorStore((s) => s.wires);
const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
const selectedWireId = useSimulatorStore((s) => s.selectedWireId);
return (
<svg
className="wire-layer"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
overflow: 'visible',
pointerEvents: 'none',
zIndex: 35,
}}
>
{wires.map((wire) => {
// Skip null/undefined wires (can happen during circuit loading)
if (!wire) return null;
return (
<WireRenderer
key={wire.id}
wire={wire}
isSelected={wire.id === selectedWireId}
isHovered={wire.id === hoveredWireId}
overridePath={
segmentDragPreview?.wireId === wire.id
? segmentDragPreview.overridePath
: undefined
}
/>
);
})}
{/* Segment handles for the selected wire — scaled inversely to zoom for constant screen size */}
{segmentHandles.map((handle) => {
const baseR = isTouchDevice ? 14 : 7;
const r = baseR / zoom;
const sw = 2 / zoom;
return (
<circle
key={handle.segIndex}
cx={handle.mx}
cy={handle.my}
r={r}
fill="white"
stroke="#007acc"
strokeWidth={sw}
style={{ pointerEvents: 'all', cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize', touchAction: 'none' }}
data-segment-handle={handle.segIndex}
onMouseDown={(e) => onHandleMouseDown(e, handle.segIndex)}
onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)}
/>
);
})}
{wireInProgress && (
<WireInProgressRenderer wireInProgress={wireInProgress} isAiming={isAiming} />
)}
</svg>
);
};