#3: `start.ts` now kicks `scheduler.start()` (lazy-boot the WASM engine) right when the editor mounts. Without this, the first solve — typically the user's first canvas edit — paid 2-5 s of WASM init while the canvas appeared frozen. Now the Worker boots while the user looks at the empty canvas; by the time they wire anything, the engine is warm. #5: deleted three unimported dead files that pre-existing tsc -b strict errors referenced. Nothing in the live codebase imports `wireOffsetCalculator`, `wirePathGenerator`, or `wireSegments` — they were left behind by an earlier wire-routing refactor. Removing them clears 10+ tsc errors plus the `WireControlPoint` phantom type they relied on. Also cleaned up an unused import in `capacitor-charge-transient.test.ts` (leftover from F2). 1461 tests pass, vite build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
54936ef660
commit
29d76348aa
|
|
@ -11,7 +11,6 @@
|
|||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildInputFromStore } from '../simulation/spice/storeAdapter';
|
||||
import type { StoreSnapshot } from '../simulation/spice/storeAdapter';
|
||||
import type { PinSourceState } from '../simulation/spice/types';
|
||||
|
||||
describe('storeAdapter — MCU-driven capacitor step response', () => {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,14 @@ export function startSimulation(): () => void {
|
|||
),
|
||||
},
|
||||
);
|
||||
// Phase 1d #3 — pre-boot the WASM engine the moment the editor
|
||||
// mounts. Without this, the first solve (typically the user's
|
||||
// first canvas edit) pays the full WASM init cost (~2-5 s) and the
|
||||
// canvas appears frozen. By kicking init now, the Worker boots
|
||||
// while the user looks at the empty canvas; by the time they wire
|
||||
// anything, the engine is warm.
|
||||
void getMixedModeScheduler().start();
|
||||
|
||||
const unsubService = service.start();
|
||||
const unsubAdc = connectAnalogInputsToMcu();
|
||||
const unsubEdges = connectMcuEdgesToService(service);
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
/**
|
||||
* 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).
|
||||
*
|
||||
* Instead of moving the endpoints (which would visually disconnect the wire from its
|
||||
* pins), we keep the true pin positions fixed and insert short stub segments that
|
||||
* travel from each pin to the offset path, forming an L-shaped attachment at both ends.
|
||||
*
|
||||
* Example (horizontal wire, offset = +6):
|
||||
* Before: pin ────────────────── pin
|
||||
* After: pin (stub)
|
||||
* │ ──────────────── │
|
||||
* (stub) pin
|
||||
*/
|
||||
export function applyOffsetToWire(wire: Wire, offset: number): Wire {
|
||||
if (offset === 0) return wire;
|
||||
|
||||
// Determine primary direction from the first segment of the path
|
||||
const firstControlOrEnd =
|
||||
wire.controlPoints && wire.controlPoints.length > 0 ? wire.controlPoints[0] : wire.end;
|
||||
|
||||
const isHorizontalFirst =
|
||||
Math.abs(firstControlOrEnd.x - wire.start.x) >= Math.abs(firstControlOrEnd.y - wire.start.y);
|
||||
|
||||
// True pin positions (never moved)
|
||||
const pinStart = { x: wire.start.x, y: wire.start.y };
|
||||
const pinEnd = { x: wire.end.x, y: wire.end.y };
|
||||
|
||||
// Offset intermediate points perpendicular to the primary direction
|
||||
const shiftedControlPoints = (wire.controlPoints || []).map((cp) => ({
|
||||
...cp,
|
||||
x: isHorizontalFirst ? cp.x : cp.x + offset,
|
||||
y: isHorizontalFirst ? cp.y + offset : cp.y,
|
||||
}));
|
||||
|
||||
// Compute where the offset path actually starts/ends
|
||||
// (the point on the parallel track immediately after the pin stub)
|
||||
const offsetStart = isHorizontalFirst
|
||||
? { x: pinStart.x, y: pinStart.y + offset }
|
||||
: { x: pinStart.x + offset, y: pinStart.y };
|
||||
|
||||
const offsetEnd = isHorizontalFirst
|
||||
? { x: pinEnd.x, y: pinEnd.y + offset }
|
||||
: { x: pinEnd.x + offset, y: pinEnd.y };
|
||||
|
||||
// Build new control points:
|
||||
// stub from pinStart → offsetStart, then the shifted intermediates, then stub from offsetEnd → pinEnd
|
||||
// We only need to add extra stubs when they are non-zero length.
|
||||
const newControlPoints: typeof wire.controlPoints = [];
|
||||
|
||||
// Leading stub end-point (where the offset path begins)
|
||||
if (offsetStart.x !== pinStart.x || offsetStart.y !== pinStart.y) {
|
||||
newControlPoints.push({ id: `${wire.id}-stub-s`, ...offsetStart });
|
||||
}
|
||||
|
||||
// Shifted original control points
|
||||
for (const cp of shiftedControlPoints) {
|
||||
newControlPoints.push(cp);
|
||||
}
|
||||
|
||||
// Trailing stub start-point (where the offset path rejoins the pin)
|
||||
if (offsetEnd.x !== pinEnd.x || offsetEnd.y !== pinEnd.y) {
|
||||
newControlPoints.push({ id: `${wire.id}-stub-e`, ...offsetEnd });
|
||||
}
|
||||
|
||||
return {
|
||||
...wire,
|
||||
start: { ...wire.start, x: pinStart.x, y: pinStart.y },
|
||||
end: { ...wire.end, x: pinEnd.x, y: pinEnd.y },
|
||||
controlPoints: newControlPoints,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/**
|
||||
* Wire Path Generator
|
||||
*
|
||||
* Generates SVG path strings for wires with orthogonal routing (only horizontal/vertical segments).
|
||||
* Phase 1: Simple L-shape
|
||||
* Phase 2: Multi-segment with control points
|
||||
* Phase 3: A* pathfinding integration
|
||||
*/
|
||||
|
||||
import type { Wire, WireControlPoint } from '../types/wire';
|
||||
|
||||
/**
|
||||
* Generates an SVG path string for a wire.
|
||||
* Routes wires using orthogonal paths (90-degree angles only).
|
||||
*
|
||||
* @param wire - The wire object containing endpoints and control points
|
||||
* @returns SVG path string (e.g., "M 10 20 L 30 20 L 30 50")
|
||||
*/
|
||||
export function generateWirePath(wire: Wire): string {
|
||||
const { start, end, controlPoints } = wire;
|
||||
|
||||
if (controlPoints.length === 0) {
|
||||
// Phase 1: Simple L-shape routing
|
||||
return generateSimplePath(start.x, start.y, end.x, end.y);
|
||||
} else {
|
||||
// Phase 2: Multi-segment with control points
|
||||
return generateMultiSegmentPath(start, controlPoints, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1: Generates a simple L-shaped path between two points.
|
||||
* Prioritizes horizontal-first routing (goes horizontal, then vertical, then horizontal).
|
||||
*
|
||||
* Pattern:
|
||||
* Start → [horizontal] → midpoint → [vertical] → midpoint → [horizontal] → End
|
||||
*
|
||||
* @param x1 - Start X coordinate
|
||||
* @param y1 - Start Y coordinate
|
||||
* @param x2 - End X coordinate
|
||||
* @param y2 - End Y coordinate
|
||||
* @returns SVG path string
|
||||
*/
|
||||
function generateSimplePath(x1: number, y1: number, x2: number, y2: number): string {
|
||||
// Calculate midpoint X (for L-shape bend)
|
||||
const midX = x1 + (x2 - x1) / 2;
|
||||
|
||||
// Create horizontal-first L-shape path
|
||||
// Format: M x1,y1 L midX,y1 L midX,y2 L x2,y2
|
||||
return `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Generates path with multiple control points.
|
||||
* All segments are constrained to horizontal or vertical only (orthogonal routing).
|
||||
*
|
||||
* @param start - Start endpoint { x, y }
|
||||
* @param controlPoints - Array of control points
|
||||
* @param end - End endpoint { x, y }
|
||||
* @returns SVG path string
|
||||
*/
|
||||
function generateMultiSegmentPath(
|
||||
start: { x: number; y: number },
|
||||
controlPoints: WireControlPoint[],
|
||||
end: { x: number; y: number },
|
||||
): string {
|
||||
let path = `M ${start.x} ${start.y}`;
|
||||
|
||||
// Control points already represent exact corners, so connect them sequentially
|
||||
for (let i = 0; i < controlPoints.length; i++) {
|
||||
const cp = controlPoints[i];
|
||||
path += ` L ${cp.x} ${cp.y}`;
|
||||
}
|
||||
|
||||
// Connect last control point to end
|
||||
path += ` L ${end.x} ${end.y}`;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total length of a wire path (useful for rendering and optimization).
|
||||
*
|
||||
* @param wire - The wire object
|
||||
* @returns Total path length in pixels
|
||||
*/
|
||||
export function calculateWireLength(wire: Wire): number {
|
||||
const { start, end, controlPoints } = wire;
|
||||
|
||||
let totalLength = 0;
|
||||
let prevPoint = start;
|
||||
|
||||
for (const cp of controlPoints) {
|
||||
totalLength += Math.abs(cp.x - prevPoint.x) + Math.abs(cp.y - prevPoint.y);
|
||||
prevPoint = cp;
|
||||
}
|
||||
|
||||
totalLength += Math.abs(end.x - prevPoint.x) + Math.abs(end.y - prevPoint.y);
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
/**
|
||||
* Wire Segment Utilities
|
||||
*
|
||||
* Handles computation and manipulation of wire segments for interactive editing.
|
||||
* Segments are the straight horizontal/vertical lines between path points.
|
||||
*/
|
||||
|
||||
import type { Wire, WireControlPoint } from '../types/wire';
|
||||
|
||||
export interface WireSegment {
|
||||
id: string;
|
||||
startPoint: { x: number; y: number };
|
||||
endPoint: { x: number; y: number };
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
midPoint: { x: number; y: number };
|
||||
length: number;
|
||||
startIndex: number; // Index in orthoPoints array
|
||||
endIndex: number; // Index in orthoPoints array
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all path points (start + control points + end)
|
||||
*/
|
||||
export function getPathPoints(wire: Wire): Array<{ x: number; y: number }> {
|
||||
const points: Array<{ x: number; y: number }> = [];
|
||||
|
||||
points.push({ x: wire.start.x, y: wire.start.y });
|
||||
|
||||
for (const cp of wire.controlPoints) {
|
||||
points.push({ x: cp.x, y: cp.y });
|
||||
}
|
||||
|
||||
points.push({ x: wire.end.x, y: wire.end.y });
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate orthogonal path points from control points
|
||||
* Converts diagonal connections to L-shapes (horizontal then vertical or vice versa)
|
||||
*/
|
||||
export function generateOrthogonalPoints(
|
||||
points: Array<{ x: number; y: number }>,
|
||||
): Array<{ x: number; y: number }> {
|
||||
const result: Array<{ x: number; y: number }> = [];
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const current = points[i];
|
||||
const next = points[i + 1];
|
||||
|
||||
result.push(current);
|
||||
|
||||
// If points are not aligned, add intermediate point
|
||||
if (current.x !== next.x && current.y !== next.y) {
|
||||
const dx = Math.abs(next.x - current.x);
|
||||
const dy = Math.abs(next.y - current.y);
|
||||
|
||||
if (dx > dy) {
|
||||
// Go horizontal first
|
||||
result.push({ x: next.x, y: current.y });
|
||||
} else {
|
||||
// Go vertical first
|
||||
result.push({ x: current.x, y: next.y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(points[points.length - 1]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all segments from a wire
|
||||
*/
|
||||
export function computeSegments(wire: Wire): WireSegment[] {
|
||||
const pathPoints = getPathPoints(wire);
|
||||
const orthoPoints = generateOrthogonalPoints(pathPoints);
|
||||
const segments: WireSegment[] = [];
|
||||
|
||||
for (let i = 0; i < orthoPoints.length - 1; i++) {
|
||||
const start = orthoPoints[i];
|
||||
const end = orthoPoints[i + 1];
|
||||
|
||||
// Skip zero-length segments
|
||||
if (start.x === end.x && start.y === end.y) continue;
|
||||
|
||||
const orientation = start.y === end.y ? 'horizontal' : 'vertical';
|
||||
const length =
|
||||
orientation === 'horizontal' ? Math.abs(end.x - start.x) : Math.abs(end.y - start.y);
|
||||
|
||||
segments.push({
|
||||
id: `${wire.id}-seg-${i}`,
|
||||
startPoint: start,
|
||||
endPoint: end,
|
||||
orientation,
|
||||
midPoint: {
|
||||
x: (start.x + end.x) / 2,
|
||||
y: (start.y + end.y) / 2,
|
||||
},
|
||||
length,
|
||||
startIndex: i,
|
||||
endIndex: i + 1,
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which segment is under the cursor
|
||||
*/
|
||||
export function findSegmentUnderCursor(
|
||||
segments: WireSegment[],
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
threshold: number = 8, // 8px tolerance
|
||||
): WireSegment | null {
|
||||
for (const segment of segments) {
|
||||
if (segment.orientation === 'horizontal') {
|
||||
const minX = Math.min(segment.startPoint.x, segment.endPoint.x);
|
||||
const maxX = Math.max(segment.startPoint.x, segment.endPoint.x);
|
||||
const lineY = segment.startPoint.y;
|
||||
|
||||
if (mouseX >= minX && mouseX <= maxX && Math.abs(mouseY - lineY) <= threshold) {
|
||||
return segment;
|
||||
}
|
||||
} else {
|
||||
const minY = Math.min(segment.startPoint.y, segment.endPoint.y);
|
||||
const maxY = Math.max(segment.startPoint.y, segment.endPoint.y);
|
||||
const lineX = segment.startPoint.x;
|
||||
|
||||
if (mouseY >= minY && mouseY <= maxY && Math.abs(mouseX - lineX) <= threshold) {
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update orthogonal points when dragging a segment
|
||||
*/
|
||||
export function updateOrthogonalPointsForSegmentDrag(
|
||||
orthoPoints: Array<{ x: number; y: number }>,
|
||||
segment: WireSegment,
|
||||
offset: number,
|
||||
): Array<{ x: number; y: number }> {
|
||||
const newPoints = orthoPoints.map((p) => ({ ...p }));
|
||||
const { startIndex, endIndex, orientation } = segment;
|
||||
|
||||
const newStart = { ...newPoints[startIndex] };
|
||||
const newEnd = { ...newPoints[endIndex] };
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
newStart.y += offset;
|
||||
newEnd.y += offset;
|
||||
} else {
|
||||
newStart.x += offset;
|
||||
newEnd.x += offset;
|
||||
}
|
||||
|
||||
const resultPoints: Array<{ x: number; y: number }> = [];
|
||||
|
||||
// Add points before the dragged segment
|
||||
for (let i = 0; i < startIndex; i++) {
|
||||
resultPoints.push(newPoints[i]);
|
||||
}
|
||||
|
||||
// If dragging the first segment, inject the original start pin to act as a stub anchor
|
||||
if (startIndex === 0 && offset !== 0) {
|
||||
resultPoints.push({ ...newPoints[0] });
|
||||
}
|
||||
|
||||
// Add the dragged segment's end points
|
||||
resultPoints.push(newStart);
|
||||
resultPoints.push(newEnd);
|
||||
|
||||
// If dragging the last segment, inject the original end pin to act as a stub anchor
|
||||
if (endIndex === newPoints.length - 1 && offset !== 0) {
|
||||
resultPoints.push({ ...newPoints[newPoints.length - 1] });
|
||||
}
|
||||
|
||||
// Add points after the dragged segment
|
||||
for (let i = endIndex + 1; i < newPoints.length; i++) {
|
||||
resultPoints.push(newPoints[i]);
|
||||
}
|
||||
|
||||
return resultPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert orthogonal points back to control points
|
||||
* Removes start/end points and intermediate points that are redundant
|
||||
*
|
||||
* IMPORTANT: The first and last orthoPoints should match the wire endpoints.
|
||||
* We preserve ALL intermediate points that represent corners (direction changes).
|
||||
*/
|
||||
export function orthogonalPointsToControlPoints(
|
||||
orthoPoints: Array<{ x: number; y: number }>,
|
||||
_start: { x: number; y: number },
|
||||
_end: { x: number; y: number },
|
||||
): WireControlPoint[] {
|
||||
if (orthoPoints.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove first and last points (those are start/end endpoints)
|
||||
const innerPoints = orthoPoints.slice(1, -1);
|
||||
|
||||
if (innerPoints.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Keep only corner points (where direction changes)
|
||||
const controlPoints: WireControlPoint[] = [];
|
||||
|
||||
for (let i = 0; i < innerPoints.length; i++) {
|
||||
const current = innerPoints[i];
|
||||
// Get prev from orthoPoints (index i in innerPoints = index i+1 in orthoPoints)
|
||||
const prev = orthoPoints[i]; // Previous point in orthoPoints
|
||||
const next = orthoPoints[i + 2]; // Next point in orthoPoints
|
||||
|
||||
// Check if current point is a corner (changes direction)
|
||||
// We use a cross product check to tolerate slight non-90-degree angles during drag
|
||||
const dx1 = current.x - prev.x;
|
||||
const dy1 = current.y - prev.y;
|
||||
const dx2 = next.x - current.x;
|
||||
const dy2 = next.y - current.y;
|
||||
|
||||
// Cross product magnitude. If > 0.1, the path bends here.
|
||||
const isCorner = Math.abs(dx1 * dy2 - dy1 * dx2) > 0.1;
|
||||
|
||||
if (isCorner) {
|
||||
controlPoints.push({
|
||||
id: `cp-${Date.now()}-${i}`,
|
||||
x: current.x,
|
||||
y: current.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return controlPoints;
|
||||
}
|
||||
Loading…
Reference in New Issue