164 lines
5.0 KiB
TypeScript
164 lines
5.0 KiB
TypeScript
import React from 'react';
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
|
import { WireRenderer } from './WireRenderer';
|
|
import { WireInProgressRenderer } from './WireInProgressRenderer';
|
|
import { useIsCoarsePointer } from '../../utils/useTouchDevice';
|
|
|
|
export interface SegmentHandle {
|
|
segIndex: number;
|
|
axis: 'horizontal' | 'vertical';
|
|
mx: number; // midpoint X
|
|
my: number; // midpoint Y
|
|
}
|
|
|
|
export interface WaypointHandle {
|
|
/** Index of this waypoint in the wire's waypoints[] array */
|
|
index: number;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface AlignmentGuide {
|
|
axis: 'x' | 'y';
|
|
/** World coordinate of the guide line (x for vertical, y for horizontal) */
|
|
value: number;
|
|
}
|
|
|
|
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[];
|
|
/** Bend-point handles to render for the selected wire */
|
|
waypointHandles: WaypointHandle[];
|
|
/** Alignment guides shown while dragging */
|
|
alignmentGuides?: AlignmentGuide[];
|
|
/** Called when user starts dragging a segment handle (passes segIndex) */
|
|
onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void;
|
|
/** Called when user starts dragging a segment handle via touch (passes segIndex) */
|
|
onHandleTouchStart?: (e: React.TouchEvent, segIndex: number) => void;
|
|
/** Called when user starts dragging a waypoint handle */
|
|
onWaypointMouseDown: (e: React.MouseEvent, waypointIndex: number) => void;
|
|
/** Called when user starts dragging a waypoint handle via touch */
|
|
onWaypointTouchStart?: (e: React.TouchEvent, waypointIndex: number) => void;
|
|
}
|
|
|
|
export const WireLayer: React.FC<WireLayerProps> = ({
|
|
hoveredWireId,
|
|
segmentDragPreview,
|
|
segmentHandles,
|
|
waypointHandles,
|
|
alignmentGuides,
|
|
onHandleMouseDown,
|
|
onHandleTouchStart,
|
|
onWaypointMouseDown,
|
|
onWaypointTouchStart,
|
|
}) => {
|
|
const wires = useSimulatorStore((s) => s.wires);
|
|
const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
|
|
const selectedWireId = useSimulatorStore((s) => s.selectedWireId);
|
|
const isTouchDevice = useIsCoarsePointer();
|
|
|
|
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) => (
|
|
<WireRenderer
|
|
key={wire.id}
|
|
wire={wire}
|
|
isSelected={wire.id === selectedWireId}
|
|
isHovered={wire.id === hoveredWireId}
|
|
overridePath={
|
|
segmentDragPreview?.wireId === wire.id ? segmentDragPreview.overridePath : undefined
|
|
}
|
|
/>
|
|
))}
|
|
|
|
{/* Alignment guides — full-canvas dashed lines snap-targets while dragging */}
|
|
{alignmentGuides?.map((g, i) =>
|
|
g.axis === 'x' ? (
|
|
<line
|
|
key={`guide-${i}`}
|
|
x1={g.value}
|
|
y1={-100000}
|
|
x2={g.value}
|
|
y2={100000}
|
|
stroke="#00d9ff"
|
|
strokeWidth={1}
|
|
strokeDasharray="4,4"
|
|
opacity={0.85}
|
|
/>
|
|
) : (
|
|
<line
|
|
key={`guide-${i}`}
|
|
x1={-100000}
|
|
y1={g.value}
|
|
x2={100000}
|
|
y2={g.value}
|
|
stroke="#00d9ff"
|
|
strokeWidth={1}
|
|
strokeDasharray="4,4"
|
|
opacity={0.85}
|
|
/>
|
|
),
|
|
)}
|
|
|
|
{/* Segment handles for the selected wire */}
|
|
{segmentHandles.map((handle) => (
|
|
<circle
|
|
key={`seg-${handle.segIndex}`}
|
|
data-wire-handle="segment"
|
|
cx={handle.mx}
|
|
cy={handle.my}
|
|
r={isTouchDevice ? 14 : 7}
|
|
fill="white"
|
|
stroke="#007acc"
|
|
strokeWidth={2}
|
|
style={{
|
|
pointerEvents: 'all',
|
|
cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize',
|
|
touchAction: 'none',
|
|
}}
|
|
onMouseDown={(e) => onHandleMouseDown(e, handle.segIndex)}
|
|
onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)}
|
|
/>
|
|
))}
|
|
|
|
{/* Waypoint handles — bend points that drag freely in any direction */}
|
|
{waypointHandles.map((handle) => (
|
|
<circle
|
|
key={`wp-${handle.index}`}
|
|
data-wire-handle="waypoint"
|
|
cx={handle.x}
|
|
cy={handle.y}
|
|
r={isTouchDevice ? 12 : 6}
|
|
fill="#007acc"
|
|
stroke="white"
|
|
strokeWidth={2}
|
|
style={{
|
|
pointerEvents: 'all',
|
|
cursor: 'move',
|
|
touchAction: 'none',
|
|
}}
|
|
onMouseDown={(e) => onWaypointMouseDown(e, handle.index)}
|
|
onTouchStart={(e) => onWaypointTouchStart?.(e, handle.index)}
|
|
/>
|
|
))}
|
|
|
|
{wireInProgress && <WireInProgressRenderer wireInProgress={wireInProgress} />}
|
|
</svg>
|
|
);
|
|
};
|