feat: add mobile touch support for wire creation and dragging
Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>
This commit is contained in:
parent
357c43ae0d
commit
b821fe9747
|
|
@ -1822,7 +1822,7 @@
|
||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -1838,14 +1838,6 @@
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
|
||||||
"version": "2.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.1",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
|
|
@ -2723,7 +2715,7 @@
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
|
|
@ -2781,16 +2773,6 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
|
||||||
"version": "3.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
|
||||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
||||||
"peer": true,
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@types/trusted-types": "^2.0.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -3870,19 +3852,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
|
||||||
"version": "14.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
|
||||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"marked": "bin/marked.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -3933,17 +3902,6 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/monaco-editor": {
|
|
||||||
"version": "0.55.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
|
||||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"dompurify": "3.2.7",
|
|
||||||
"marked": "14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,18 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
|
||||||
const pinX = pin.x;
|
const pinX = pin.x;
|
||||||
const pinY = pin.y;
|
const pinY = pin.y;
|
||||||
|
|
||||||
|
const handleActivate = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pin.name}
|
key={pin.name}
|
||||||
onClick={(e) => {
|
data-pin-component-id={componentId}
|
||||||
e.stopPropagation();
|
data-pin-name={pin.name}
|
||||||
onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY);
|
onClick={handleActivate}
|
||||||
}}
|
onTouchEnd={handleActivate}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${pinX - 4}px`,
|
left: `${pinX - 4}px`,
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,28 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Cancel wire button ──────────────────────────── */
|
||||||
|
.cancel-wire-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
background: #c0392b;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-wire-btn:hover {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Canvas content (viewport) ───────────────────── */
|
/* ── Canvas content (viewport) ───────────────────── */
|
||||||
.canvas-content {
|
.canvas-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,18 @@ export const SimulatorCanvas = () => {
|
||||||
const boardPositionRef = useRef(boardPosition);
|
const boardPositionRef = useRef(boardPosition);
|
||||||
boardPositionRef.current = boardPosition;
|
boardPositionRef.current = boardPosition;
|
||||||
|
|
||||||
|
// Refs for wire actions — needed inside native touch closures
|
||||||
|
const wireInProgressRef = useRef(wireInProgress);
|
||||||
|
wireInProgressRef.current = wireInProgress;
|
||||||
|
const startWireCreationRef = useRef(startWireCreation);
|
||||||
|
startWireCreationRef.current = startWireCreation;
|
||||||
|
const finishWireCreationRef = useRef(finishWireCreation);
|
||||||
|
finishWireCreationRef.current = finishWireCreation;
|
||||||
|
const cancelWireCreationRef = useRef(cancelWireCreation);
|
||||||
|
cancelWireCreationRef.current = cancelWireCreation;
|
||||||
|
const updateWireInProgressRef = useRef(updateWireInProgress);
|
||||||
|
updateWireInProgressRef.current = updateWireInProgress;
|
||||||
|
|
||||||
// Touch-specific state refs (for single-finger drag and pinch-to-zoom)
|
// Touch-specific state refs (for single-finger drag and pinch-to-zoom)
|
||||||
const touchDraggedComponentIdRef = useRef<string | null>(null);
|
const touchDraggedComponentIdRef = useRef<string | null>(null);
|
||||||
const touchDragOffsetRef = useRef({ x: 0, y: 0 });
|
const touchDragOffsetRef = useRef({ x: 0, y: 0 });
|
||||||
|
|
@ -103,6 +115,8 @@ export const SimulatorCanvas = () => {
|
||||||
const pinchStartZoomRef = useRef(1);
|
const pinchStartZoomRef = useRef(1);
|
||||||
const pinchStartMidRef = useRef({ x: 0, y: 0 });
|
const pinchStartMidRef = useRef({ x: 0, y: 0 });
|
||||||
const pinchStartPanRef = useRef({ x: 0, y: 0 });
|
const pinchStartPanRef = useRef({ x: 0, y: 0 });
|
||||||
|
// Flag: the most recent single-finger touch started on a pin element
|
||||||
|
const touchStartedOnPinRef = useRef(false);
|
||||||
|
|
||||||
// Convert viewport coords to world (canvas) coords
|
// Convert viewport coords to world (canvas) coords
|
||||||
const toWorld = useCallback((screenX: number, screenY: number) => {
|
const toWorld = useCallback((screenX: number, screenY: number) => {
|
||||||
|
|
@ -176,12 +190,26 @@ export const SimulatorCanvas = () => {
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
touchClickStartTimeRef.current = Date.now();
|
touchClickStartTimeRef.current = Date.now();
|
||||||
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
touchStartedOnPinRef.current = false;
|
||||||
|
|
||||||
// Identify what element was touched
|
// Identify what element was touched
|
||||||
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||||
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
||||||
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
||||||
|
|
||||||
|
// ── Single finger on a pin: handle wire creation (do not pan) ──
|
||||||
|
const pinEl = target?.closest('[data-pin-component-id]') as HTMLElement | null;
|
||||||
|
if (pinEl && !runningRef.current) {
|
||||||
|
touchStartedOnPinRef.current = true;
|
||||||
|
return; // PinOverlay's onTouchEnd will call onPinClick
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Single finger on a wire element: do not pan (WireRenderer handles it) ──
|
||||||
|
const noPanEl = target?.closest('[data-no-pan]');
|
||||||
|
if (noPanEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (componentWrapper && !runningRef.current) {
|
if (componentWrapper && !runningRef.current) {
|
||||||
// ── Single finger on a component: start drag ──
|
// ── Single finger on a component: start drag ──
|
||||||
const componentId = componentWrapper.getAttribute('data-component-id');
|
const componentId = componentWrapper.getAttribute('data-component-id');
|
||||||
|
|
@ -278,6 +306,10 @@ export const SimulatorCanvas = () => {
|
||||||
y: world.y - touchDragOffsetRef.current.y,
|
y: world.y - touchDragOffsetRef.current.y,
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
} else if (wireInProgressRef.current) {
|
||||||
|
// ── Single finger wire-in-progress preview ──
|
||||||
|
const world = toWorld(touch.clientX, touch.clientY);
|
||||||
|
updateWireInProgressRef.current(world.x, world.y);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -325,16 +357,23 @@ export const SimulatorCanvas = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Short tap on empty canvas: deselect ──
|
// ── Short tap on empty canvas ──
|
||||||
if (changed) {
|
if (changed) {
|
||||||
const elapsed = Date.now() - touchClickStartTimeRef.current;
|
const elapsed = Date.now() - touchClickStartTimeRef.current;
|
||||||
const dx = changed.clientX - touchClickStartPosRef.current.x;
|
const dx = changed.clientX - touchClickStartPosRef.current.x;
|
||||||
const dy = changed.clientY - touchClickStartPosRef.current.y;
|
const dy = changed.clientY - touchClickStartPosRef.current.y;
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
if (dist < 5 && elapsed < 300) {
|
if (dist < 5 && elapsed < 300) {
|
||||||
setSelectedComponentId(null);
|
// If wire creation is in progress and the tap was NOT on a pin,
|
||||||
|
// cancel the wire (acts like pressing Escape on desktop).
|
||||||
|
if (wireInProgressRef.current && !touchStartedOnPinRef.current) {
|
||||||
|
cancelWireCreationRef.current();
|
||||||
|
} else {
|
||||||
|
setSelectedComponentId(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
touchStartedOnPinRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
el.addEventListener('touchstart', onTouchStart, { passive: false });
|
el.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||||
|
|
@ -782,6 +821,17 @@ export const SimulatorCanvas = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="canvas-header-right">
|
<div className="canvas-header-right">
|
||||||
|
{/* Cancel wire creation — shown when a wire is being drawn (especially useful on mobile) */}
|
||||||
|
{wireInProgress && (
|
||||||
|
<button
|
||||||
|
className="cancel-wire-btn"
|
||||||
|
onClick={cancelWireCreation}
|
||||||
|
title="Cancel wire (Esc)"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
||||||
|
Cancel wire
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Zoom controls */}
|
{/* Zoom controls */}
|
||||||
<div className="zoom-controls">
|
<div className="zoom-controls">
|
||||||
<button className="zoom-btn" onClick={() => handleWheel({ deltaY: 100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom out">
|
<button className="zoom-btn" onClick={() => handleWheel({ deltaY: 100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom out">
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,15 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
[wire.id, setSelectedWire]
|
[wire.id, setSelectedWire]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle wire selection via touch tap
|
||||||
|
const handleWireTouchEnd = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedWire(wire.id);
|
||||||
|
},
|
||||||
|
[wire.id, setSelectedWire]
|
||||||
|
);
|
||||||
|
|
||||||
// Handle segment hover
|
// Handle segment hover
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
|
@ -170,6 +179,47 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
[dragState, isSelected, segments, wire, updateWire]
|
[dragState, isSelected, segments, wire, updateWire]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle touch move for segment drag
|
||||||
|
const handleTouchMove = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (!dragState) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
if (!touch) return;
|
||||||
|
|
||||||
|
if (rafRef.current !== null) {
|
||||||
|
cancelAnimationFrame(rafRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
rafRef.current = requestAnimationFrame(() => {
|
||||||
|
const svg = svgRef.current?.ownerSVGElement;
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
const svgRect = svg.getBoundingClientRect();
|
||||||
|
const touchX = touch.clientX - svgRect.left;
|
||||||
|
const touchY = touch.clientY - svgRect.top;
|
||||||
|
|
||||||
|
const { segment, startMousePos, originalOrthoPoints } = dragState;
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
if (segment.orientation === 'horizontal') {
|
||||||
|
offset = touchY - startMousePos.y;
|
||||||
|
} else {
|
||||||
|
offset = touchX - startMousePos.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOrthoPoints = updateOrthogonalPointsForSegmentDrag(
|
||||||
|
originalOrthoPoints,
|
||||||
|
segment,
|
||||||
|
offset
|
||||||
|
);
|
||||||
|
|
||||||
|
setPreviewOrthoPoints(newOrthoPoints);
|
||||||
|
rafRef.current = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dragState]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSegmentMouseDown = useCallback(
|
const handleSegmentMouseDown = useCallback(
|
||||||
(segment: WireSegment, e: React.MouseEvent) => {
|
(segment: WireSegment, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -206,6 +256,32 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
[wire]
|
[wire]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle touch start on a segment — begins drag on mobile
|
||||||
|
const handleSegmentTouchStart = useCallback(
|
||||||
|
(segment: WireSegment, e: React.TouchEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
if (!touch) return;
|
||||||
|
|
||||||
|
const svg = svgRef.current?.ownerSVGElement;
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
const svgRect = svg.getBoundingClientRect();
|
||||||
|
const touchX = touch.clientX - svgRect.left;
|
||||||
|
const touchY = touch.clientY - svgRect.top;
|
||||||
|
|
||||||
|
const pathPoints = getPathPoints(wire);
|
||||||
|
const orthoPoints = generateOrthogonalPoints(pathPoints);
|
||||||
|
|
||||||
|
setDragState({
|
||||||
|
segment,
|
||||||
|
startMousePos: { x: touchX, y: touchY },
|
||||||
|
originalOrthoPoints: orthoPoints,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[wire]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
const handleMouseUp = useCallback(() => {
|
||||||
console.log('Mouse Up - Drag State:', {
|
console.log('Mouse Up - Drag State:', {
|
||||||
hasDragState: !!dragState,
|
hasDragState: !!dragState,
|
||||||
|
|
@ -247,6 +323,28 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
setPreviewOrthoPoints(null);
|
setPreviewOrthoPoints(null);
|
||||||
}, [dragState, previewOrthoPoints, wire, updateWire]);
|
}, [dragState, previewOrthoPoints, wire, updateWire]);
|
||||||
|
|
||||||
|
// Touch equivalent of handleMouseUp — finishes segment drag on mobile
|
||||||
|
const handleTouchEnd = useCallback(() => {
|
||||||
|
if (dragState && previewOrthoPoints) {
|
||||||
|
const GRID_SIZE = 20;
|
||||||
|
const snappedPoints = previewOrthoPoints.map((p) => ({
|
||||||
|
x: Math.round(p.x / GRID_SIZE) * GRID_SIZE,
|
||||||
|
y: Math.round(p.y / GRID_SIZE) * GRID_SIZE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const newControlPoints = orthogonalPointsToControlPoints(
|
||||||
|
snappedPoints,
|
||||||
|
wire.start,
|
||||||
|
wire.end
|
||||||
|
);
|
||||||
|
|
||||||
|
updateWire(wire.id, { controlPoints: newControlPoints });
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragState(null);
|
||||||
|
setPreviewOrthoPoints(null);
|
||||||
|
}, [dragState, previewOrthoPoints, wire, updateWire]);
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
if (!dragState) {
|
if (!dragState) {
|
||||||
setHoveredSegment(null);
|
setHoveredSegment(null);
|
||||||
|
|
@ -282,11 +380,14 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
<g
|
<g
|
||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
className="wire-group"
|
className="wire-group"
|
||||||
|
data-no-pan="true"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
>
|
>
|
||||||
{/* Invisible thick path for easier clicking */}
|
{/* Invisible thick path for easier clicking/tapping */}
|
||||||
<path
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
|
|
@ -294,6 +395,7 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
fill="none"
|
fill="none"
|
||||||
style={{ pointerEvents: 'stroke', cursor: 'pointer' }}
|
style={{ pointerEvents: 'stroke', cursor: 'pointer' }}
|
||||||
onClick={handleWireClick}
|
onClick={handleWireClick}
|
||||||
|
onTouchEnd={handleWireTouchEnd}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Background erasing path for visual crossing effect */}
|
{/* Background erasing path for visual crossing effect */}
|
||||||
|
|
@ -357,6 +459,7 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
pointerEvents: 'stroke',
|
pointerEvents: 'stroke',
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => handleSegmentMouseDown(segment, e)}
|
onMouseDown={(e) => handleSegmentMouseDown(segment, e)}
|
||||||
|
onTouchStart={(e) => handleSegmentTouchStart(segment, e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Visual drag handle at midpoint when hovering */}
|
{/* Visual drag handle at midpoint when hovering */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue