104 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
};
|