feat: add react-router-dom for routing and enhance wire rendering with automatic offsets
parent
1269550e8a
commit
c2f07665b4
|
|
@ -14,6 +14,7 @@
|
|||
"axios": "^1.13.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -2127,6 +2128,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -3296,6 +3310,44 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
||||
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
|
||||
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
|
@ -3322,6 +3374,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"axios": "^1.13.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
const mountedRef = useRef(false);
|
||||
|
||||
const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent);
|
||||
const running = useSimulatorStore((s) => s.running);
|
||||
|
||||
// Check if component is interactive (has simulation logic with attachEvents)
|
||||
const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]);
|
||||
const isInteractive = logic?.attachEvents !== undefined;
|
||||
|
||||
/**
|
||||
* Sync React properties to Web Component
|
||||
|
|
@ -179,11 +184,8 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
|
||||
const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); // Fallback if id is like led-1
|
||||
|
||||
console.log(`[DynamicComponent] Component ${id} (${metadata.id}): Logic found =`, !!logic);
|
||||
|
||||
let cleanupSimulationEvents: (() => void) | undefined;
|
||||
if (logic && logic.attachEvents) {
|
||||
console.log(`[DynamicComponent] Attaching events for ${id} (${metadata.id})`);
|
||||
// We need AVRSimulator instance. We can grab it from store.
|
||||
const simulator = useSimulatorStore.getState().simulator;
|
||||
if (simulator) {
|
||||
|
|
@ -225,7 +227,7 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
position: 'absolute',
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
cursor: 'move',
|
||||
cursor: running && isInteractive ? 'pointer' : 'move',
|
||||
border: isSelected ? '2px dashed #007acc' : '2px solid transparent',
|
||||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
|
|
|
|||
|
|
@ -303,6 +303,20 @@ export const SimulatorCanvas = () => {
|
|||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [wireInProgress, cancelWireCreation]);
|
||||
|
||||
// Recalculate wire positions when components change (e.g., when loading an example)
|
||||
useEffect(() => {
|
||||
// Wait for components to render and pinInfo to be available
|
||||
// Use multiple retries to ensure pinInfo is ready
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
|
||||
// Try at 100ms, 300ms, and 500ms to ensure all components have rendered
|
||||
timers.push(setTimeout(() => recalculateAllWirePositions(), 100));
|
||||
timers.push(setTimeout(() => recalculateAllWirePositions(), 300));
|
||||
timers.push(setTimeout(() => recalculateAllWirePositions(), 500));
|
||||
|
||||
return () => timers.forEach(t => clearTimeout(t));
|
||||
}, [components, recalculateAllWirePositions]);
|
||||
|
||||
// Render component using dynamic renderer
|
||||
const renderComponent = (component: any) => {
|
||||
const metadata = registry.getById(component.metadataId);
|
||||
|
|
@ -325,21 +339,29 @@ export const SimulatorCanvas = () => {
|
|||
y={component.y}
|
||||
isSelected={isSelected}
|
||||
onMouseDown={(e) => {
|
||||
handleComponentMouseDown(component.id, e);
|
||||
// Only handle UI events when simulation is NOT running
|
||||
if (!running) {
|
||||
handleComponentMouseDown(component.id, e);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
handleComponentDoubleClick(component.id, e);
|
||||
// Only handle UI events when simulation is NOT running
|
||||
if (!running) {
|
||||
handleComponentDoubleClick(component.id, e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pin overlay for wire creation */}
|
||||
<PinOverlay
|
||||
componentId={component.id}
|
||||
componentX={component.x}
|
||||
componentY={component.y}
|
||||
onPinClick={handlePinClick}
|
||||
showPins={showPinsForComponent}
|
||||
/>
|
||||
{/* Pin overlay for wire creation - hide when running */}
|
||||
{!running && (
|
||||
<PinOverlay
|
||||
componentId={component.id}
|
||||
componentX={component.x}
|
||||
componentY={component.y}
|
||||
onPinClick={handlePinClick}
|
||||
showPins={showPinsForComponent}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,16 +3,36 @@
|
|||
*
|
||||
* SVG layer that renders all wires below components.
|
||||
* Positioned absolutely with full canvas coverage.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic offset calculation for overlapping wires
|
||||
* - Visual separation of parallel wires
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { WireRenderer } from './WireRenderer';
|
||||
import { WireInProgressRenderer } from './WireInProgressRenderer';
|
||||
import { calculateWireOffsets, applyOffsetToWire } from '../../utils/wireOffsetCalculator';
|
||||
|
||||
export const WireLayer: React.FC = () => {
|
||||
const { wires, wireInProgress, selectedWireId } = useSimulatorStore();
|
||||
|
||||
// Calculate automatic offsets for overlapping wires
|
||||
const wireOffsets = useMemo(() => {
|
||||
return calculateWireOffsets(wires);
|
||||
}, [wires]);
|
||||
|
||||
// Apply offsets to wires for rendering
|
||||
// Priority: manual offset > automatic offset > 0
|
||||
const offsetWires = useMemo(() => {
|
||||
return wires.map(wire => {
|
||||
const automaticOffset = wireOffsets.get(wire.id) || 0;
|
||||
const finalOffset = wire.manualOffset !== undefined ? wire.manualOffset : automaticOffset;
|
||||
return applyOffsetToWire(wire, finalOffset);
|
||||
});
|
||||
}, [wires, wireOffsets]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="wire-layer"
|
||||
|
|
@ -34,8 +54,8 @@ export const WireLayer: React.FC = () => {
|
|||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Render all wires */}
|
||||
{wires.map((wire) => (
|
||||
{/* Render all wires with automatic offsets */}
|
||||
{offsetWires.map((wire, index) => (
|
||||
<WireRenderer
|
||||
key={wire.id}
|
||||
wire={wire}
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ void loop() {
|
|||
{
|
||||
id: 'wire-button',
|
||||
start: { componentId: 'arduino-uno', pinName: '2' },
|
||||
end: { componentId: 'button-1', pinName: '1.L' },
|
||||
end: { componentId: 'button-1', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
{
|
||||
|
|
@ -570,25 +570,25 @@ void loop() {
|
|||
{
|
||||
id: 'wire-button-red',
|
||||
start: { componentId: 'arduino-uno', pinName: '2' },
|
||||
end: { componentId: 'button-red', pinName: '1.L' },
|
||||
end: { componentId: 'button-red', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
{
|
||||
id: 'wire-button-green',
|
||||
start: { componentId: 'arduino-uno', pinName: '3' },
|
||||
end: { componentId: 'button-green', pinName: '1.L' },
|
||||
end: { componentId: 'button-green', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
{
|
||||
id: 'wire-button-blue',
|
||||
start: { componentId: 'arduino-uno', pinName: '4' },
|
||||
end: { componentId: 'button-blue', pinName: '1.L' },
|
||||
end: { componentId: 'button-blue', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
{
|
||||
id: 'wire-button-yellow',
|
||||
start: { componentId: 'arduino-uno', pinName: '5' },
|
||||
end: { componentId: 'button-yellow', pinName: '1.L' },
|
||||
end: { componentId: 'button-yellow', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
// Recalculate all wire positions from actual DOM pinInfo
|
||||
recalculateAllWirePositions: () => {
|
||||
const state = get();
|
||||
|
||||
const updatedWires = state.wires.map((wire) => {
|
||||
const updated = { ...wire };
|
||||
const startComp = state.components.find((c) => c.id === wire.start.componentId);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export interface Wire {
|
|||
// Visual properties
|
||||
color: string; // Computed from signal type
|
||||
|
||||
// Automatic offset management (for overlapping wires)
|
||||
manualOffset?: number; // User-defined offset (overrides automatic calculation)
|
||||
|
||||
// Metadata
|
||||
signalType: WireSignalType | null; // For validation and coloring
|
||||
isValid: boolean; // Connection validation result (Phase 3)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
/**
|
||||
* Wire Offset Calculator
|
||||
*
|
||||
* Automatically calculates visual offsets for overlapping wires to prevent
|
||||
* them from rendering on top of each other (similar to Fritzing/TinkerCAD).
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Detect wire segments that are parallel and overlapping
|
||||
* 2. Group overlapping segments
|
||||
* 3. Apply perpendicular offset to each wire in the group
|
||||
* 4. Distribute offsets evenly around the center line
|
||||
*/
|
||||
|
||||
import type { Wire } from '../types/wire';
|
||||
|
||||
export const WIRE_SPACING = 6; // Pixels between parallel wires
|
||||
const OVERLAP_TOLERANCE = 5; // Pixels tolerance for considering wires as overlapping
|
||||
|
||||
/**
|
||||
* Represents a wire segment (portion of a wire between two bends)
|
||||
*/
|
||||
interface WireSegment {
|
||||
wireId: string;
|
||||
isVertical: boolean;
|
||||
start: { x: number; y: number };
|
||||
end: { x: number; y: number };
|
||||
centerLine: number; // X position for vertical, Y position for horizontal
|
||||
}
|
||||
|
||||
/**
|
||||
* Group of overlapping wire segments
|
||||
*/
|
||||
interface SegmentGroup {
|
||||
segments: WireSegment[];
|
||||
isVertical: boolean;
|
||||
centerLine: number;
|
||||
overlapStart: number; // Start of overlapping region
|
||||
overlapEnd: number; // End of overlapping region
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all segments from a wire's path
|
||||
*/
|
||||
function extractSegments(wire: Wire): WireSegment[] {
|
||||
const segments: WireSegment[] = [];
|
||||
|
||||
// Start point
|
||||
let currentPoint = { x: wire.start.x, y: wire.start.y };
|
||||
|
||||
// Add segments through control points
|
||||
if (wire.controlPoints && wire.controlPoints.length > 0) {
|
||||
for (const controlPoint of wire.controlPoints) {
|
||||
const nextPoint = { x: controlPoint.x, y: controlPoint.y };
|
||||
|
||||
// Determine if segment is vertical or horizontal
|
||||
const isVertical = Math.abs(nextPoint.x - currentPoint.x) < Math.abs(nextPoint.y - currentPoint.y);
|
||||
const centerLine = isVertical ? currentPoint.x : currentPoint.y;
|
||||
|
||||
segments.push({
|
||||
wireId: wire.id,
|
||||
isVertical,
|
||||
start: { ...currentPoint },
|
||||
end: { ...nextPoint },
|
||||
centerLine,
|
||||
});
|
||||
|
||||
currentPoint = nextPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Final segment to end point
|
||||
const endPoint = { x: wire.end.x, y: wire.end.y };
|
||||
const isVertical = Math.abs(endPoint.x - currentPoint.x) < Math.abs(endPoint.y - currentPoint.y);
|
||||
const centerLine = isVertical ? currentPoint.x : currentPoint.y;
|
||||
|
||||
segments.push({
|
||||
wireId: wire.id,
|
||||
isVertical,
|
||||
start: { ...currentPoint },
|
||||
end: { ...endPoint },
|
||||
centerLine,
|
||||
});
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two segments overlap
|
||||
*/
|
||||
function segmentsOverlap(seg1: WireSegment, seg2: WireSegment): boolean {
|
||||
// Must be same orientation
|
||||
if (seg1.isVertical !== seg2.isVertical) return false;
|
||||
|
||||
// Must be on similar center line (within tolerance)
|
||||
if (Math.abs(seg1.centerLine - seg2.centerLine) > OVERLAP_TOLERANCE) return false;
|
||||
|
||||
// Check if ranges overlap
|
||||
if (seg1.isVertical) {
|
||||
// Vertical: check Y range overlap
|
||||
const seg1MinY = Math.min(seg1.start.y, seg1.end.y);
|
||||
const seg1MaxY = Math.max(seg1.start.y, seg1.end.y);
|
||||
const seg2MinY = Math.min(seg2.start.y, seg2.end.y);
|
||||
const seg2MaxY = Math.max(seg2.start.y, seg2.end.y);
|
||||
|
||||
return !(seg1MaxY < seg2MinY || seg2MaxY < seg1MinY);
|
||||
} else {
|
||||
// Horizontal: check X range overlap
|
||||
const seg1MinX = Math.min(seg1.start.x, seg1.end.x);
|
||||
const seg1MaxX = Math.max(seg1.start.x, seg1.end.x);
|
||||
const seg2MinX = Math.min(seg2.start.x, seg2.end.x);
|
||||
const seg2MaxX = Math.max(seg2.start.x, seg2.end.x);
|
||||
|
||||
return !(seg1MaxX < seg2MinX || seg2MaxX < seg1MinX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group overlapping segments
|
||||
*/
|
||||
function groupOverlappingSegments(segments: WireSegment[]): SegmentGroup[] {
|
||||
const groups: SegmentGroup[] = [];
|
||||
const processed = new Set<string>();
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const seg1 = segments[i];
|
||||
const key1 = `${seg1.wireId}-${i}`;
|
||||
|
||||
if (processed.has(key1)) continue;
|
||||
|
||||
// Find all segments that overlap with seg1
|
||||
const group: WireSegment[] = [seg1];
|
||||
processed.add(key1);
|
||||
|
||||
for (let j = i + 1; j < segments.length; j++) {
|
||||
const seg2 = segments[j];
|
||||
const key2 = `${seg2.wireId}-${j}`;
|
||||
|
||||
if (processed.has(key2)) continue;
|
||||
|
||||
// Check if seg2 overlaps with any segment in the current group
|
||||
if (group.some(seg => segmentsOverlap(seg, seg2))) {
|
||||
group.push(seg2);
|
||||
processed.add(key2);
|
||||
}
|
||||
}
|
||||
|
||||
// Only create a group if there are at least 2 overlapping segments
|
||||
if (group.length > 1) {
|
||||
const isVertical = group[0].isVertical;
|
||||
const centerLine = group.reduce((sum, seg) => sum + seg.centerLine, 0) / group.length;
|
||||
|
||||
// Calculate overlap region
|
||||
let overlapStart: number;
|
||||
let overlapEnd: number;
|
||||
|
||||
if (isVertical) {
|
||||
overlapStart = Math.max(...group.map(seg => Math.min(seg.start.y, seg.end.y)));
|
||||
overlapEnd = Math.min(...group.map(seg => Math.max(seg.start.y, seg.end.y)));
|
||||
} else {
|
||||
overlapStart = Math.max(...group.map(seg => Math.min(seg.start.x, seg.end.x)));
|
||||
overlapEnd = Math.min(...group.map(seg => Math.max(seg.start.x, seg.end.x)));
|
||||
}
|
||||
|
||||
groups.push({
|
||||
segments: group,
|
||||
isVertical,
|
||||
centerLine,
|
||||
overlapStart,
|
||||
overlapEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate offset for each wire based on overlapping groups
|
||||
*/
|
||||
export function calculateWireOffsets(wires: Wire[]): Map<string, number> {
|
||||
const offsets = new Map<string, number>();
|
||||
|
||||
// Initialize all offsets to 0
|
||||
wires.forEach(wire => offsets.set(wire.id, 0));
|
||||
|
||||
// Extract all segments from all wires
|
||||
const allSegments: WireSegment[] = [];
|
||||
wires.forEach(wire => {
|
||||
allSegments.push(...extractSegments(wire));
|
||||
});
|
||||
|
||||
// Group overlapping segments
|
||||
const groups = groupOverlappingSegments(allSegments);
|
||||
|
||||
// Calculate offsets for each group
|
||||
groups.forEach(group => {
|
||||
const numWires = group.segments.length;
|
||||
|
||||
// Get unique wire IDs in this group
|
||||
const wireIds = [...new Set(group.segments.map(seg => seg.wireId))];
|
||||
|
||||
// Calculate offset for each wire
|
||||
wireIds.forEach((wireId, index) => {
|
||||
// Distribute offsets symmetrically around center
|
||||
// For n wires: offsets are [-spacing*(n-1)/2, ..., 0, ..., +spacing*(n-1)/2]
|
||||
const offset = (index - (numWires - 1) / 2) * WIRE_SPACING;
|
||||
|
||||
// Store the maximum absolute offset for this wire
|
||||
// (in case wire participates in multiple groups)
|
||||
const currentOffset = offsets.get(wireId) || 0;
|
||||
if (Math.abs(offset) > Math.abs(currentOffset)) {
|
||||
offsets.set(wireId, offset);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply offset to wire points (perpendicular to wire direction)
|
||||
*/
|
||||
export function applyOffsetToWire(wire: Wire, offset: number): Wire {
|
||||
if (offset === 0) return wire;
|
||||
|
||||
// Clone the wire
|
||||
const offsetWire: Wire = {
|
||||
...wire,
|
||||
start: { ...wire.start },
|
||||
end: { ...wire.end },
|
||||
controlPoints: wire.controlPoints ? [...wire.controlPoints] : [],
|
||||
};
|
||||
|
||||
// Apply offset to start and end points
|
||||
// Determine primary direction (first segment)
|
||||
const firstPoint = offsetWire.start;
|
||||
const secondPoint = offsetWire.controlPoints && offsetWire.controlPoints.length > 0
|
||||
? offsetWire.controlPoints[0]
|
||||
: offsetWire.end;
|
||||
|
||||
const isVertical = Math.abs(secondPoint.x - firstPoint.x) < Math.abs(secondPoint.y - firstPoint.y);
|
||||
|
||||
// Apply offset perpendicular to direction
|
||||
if (isVertical) {
|
||||
// Vertical wire: offset in X
|
||||
offsetWire.start.x += offset;
|
||||
offsetWire.end.x += offset;
|
||||
offsetWire.controlPoints?.forEach(point => {
|
||||
point.x += offset;
|
||||
});
|
||||
} else {
|
||||
// Horizontal wire: offset in Y
|
||||
offsetWire.start.y += offset;
|
||||
offsetWire.end.y += offset;
|
||||
offsetWire.controlPoints?.forEach(point => {
|
||||
point.y += offset;
|
||||
});
|
||||
}
|
||||
|
||||
return offsetWire;
|
||||
}
|
||||
Loading…
Reference in New Issue