Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface TraceCleanupSolverInput {

import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { is4PointRectangle } from "./is4PointRectangle"
import { removeNetSegmentDuplicates } from "./removeNetSegmentDuplicates"

/**
* Represents the different stages or steps within the trace cleanup pipeline.
Expand Down Expand Up @@ -49,11 +50,14 @@ export class TraceCleanupSolver extends BaseSolver {
constructor(solverInput: TraceCleanupSolverInput) {
super()
this.input = solverInput
this.outputTraces = [...solverInput.allTraces]
// Pre-process: remove duplicate segments that appear in multiple traces of
// the same net. These are created by SchematicTraceLinesSolver when two MSP
// connection pairs share a pin and independently route through the same
// physical segment (issue #78 — "extra trace lines in post-processing").
const dedupedTraces = removeNetSegmentDuplicates(solverInput.allTraces)
this.outputTraces = dedupedTraces
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.traceIdQueue = Array.from(
solverInput.allTraces.map((e) => e.mspPairId),
)
this.traceIdQueue = Array.from(dedupedTraces.map((e) => e.mspPairId))
}

override _step() {
Expand Down
120 changes: 120 additions & 0 deletions lib/solvers/TraceCleanupSolver/removeNetSegmentDuplicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { Point } from "graphics-debug"
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const EPS = 1e-9

const isSamePoint = (a: Point, b: Point): boolean =>
Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS

/**
* Returns a canonical key for a segment (direction-independent).
* Two segments that are the same but in opposite directions produce the same key.
*/
const segmentKey = (a: Point, b: Point): string => {
const p1 = `${a.x},${a.y}`
const p2 = `${b.x},${b.y}`
return p1 < p2 ? `${p1}|${p2}` : `${p2}|${p1}`
}

/**
* Removes duplicate segments that appear in multiple traces of the same net.
*
* When the MSP solver creates trace paths for individual pin pairs, two traces
* in the same net can route through the same physical segment (especially near
* shared pin endpoints). This creates "extra trace lines" when rendered.
*
* This function keeps each unique segment exactly once per net by removing it
* from any trace where it appears as a redundant endpoint segment (first or
* last segment), while the same segment also exists in another trace of the
* same net.
*
* Endpoint segments (leading to pin positions) are candidates for removal when
* the same segment already exists in another trace, because the net remains
* visually connected through the other trace's segment.
*/
export const removeNetSegmentDuplicates = (
traces: SolvedTracePath[],
): SolvedTracePath[] => {
// Group traces by net
const byNet = new Map<string, SolvedTracePath[]>()
for (const trace of traces) {
const netId = trace.globalConnNetId
if (!byNet.has(netId)) byNet.set(netId, [])
byNet.get(netId)!.push(trace)
}

const result: SolvedTracePath[] = []

for (const [_netId, netTraces] of byNet) {
if (netTraces.length < 2) {
result.push(...netTraces)
continue
}

// Count how many times each segment appears across all traces in this net
const segCounts = new Map<string, number>()
for (const trace of netTraces) {
const seen = new Set<string>()
for (let i = 0; i < trace.tracePath.length - 1; i++) {
const key = segmentKey(trace.tracePath[i], trace.tracePath[i + 1])
// Only count each segment once per trace (avoid inflating from within-trace dups)
if (!seen.has(key)) {
seen.add(key)
segCounts.set(key, (segCounts.get(key) ?? 0) + 1)
}
}
}

// Find segments that appear in multiple traces
const duplicateSegs = new Set<string>()
for (const [key, count] of segCounts) {
if (count > 1) duplicateSegs.add(key)
}

if (duplicateSegs.size === 0) {
result.push(...netTraces)
continue
}

// For each trace, remove duplicate endpoint segments (first or last segment
// only, to preserve interior routing integrity). The first occurrence of a
// segment across traces is kept; subsequent traces that start/end with the
// same segment have those endpoints trimmed.
const alreadyClaimedSegs = new Set<string>()

for (const trace of netTraces) {
let { tracePath } = trace

// Check if first segment is a duplicate — trim it from this trace
// (only if the path has more than 2 points, so trimming leaves a valid path)
let trimmedFront = false
if (tracePath.length > 2) {
const firstKey = segmentKey(tracePath[0], tracePath[1])
if (duplicateSegs.has(firstKey) && alreadyClaimedSegs.has(firstKey)) {
tracePath = tracePath.slice(1)
trimmedFront = true
}
}

// Check if last segment is a duplicate — trim it from this trace
if (!trimmedFront && tracePath.length > 2) {
const lastKey = segmentKey(
tracePath[tracePath.length - 2],
tracePath[tracePath.length - 1],
)
if (duplicateSegs.has(lastKey) && alreadyClaimedSegs.has(lastKey)) {
tracePath = tracePath.slice(0, -1)
}
}

// Register all segments in this trace as claimed
for (let i = 0; i < tracePath.length - 1; i++) {
alreadyClaimedSegs.add(segmentKey(tracePath[i], tracePath[i + 1]))
}

result.push({ ...trace, tracePath })
}
}

return result
}
25 changes: 25 additions & 0 deletions lib/solvers/TraceCleanupSolver/simplifyPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,32 @@ import {
isVertical,
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"

const EPS = 1e-9

const isSamePoint = (a: Point, b: Point): boolean =>
Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS

/**
* Removes consecutive duplicate points from a path (zero-length segments).
* These can appear when path segments are concatenated at shared endpoints.
*/
export const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => {
if (path.length < 2) return path
const result: Point[] = [path[0]]
for (let i = 1; i < path.length; i++) {
if (!isSamePoint(result[result.length - 1], path[i])) {
result.push(path[i])
}
}
return result
}

export const simplifyPath = (path: Point[]): Point[] => {
// First remove any duplicate consecutive points (zero-length segments)
const dedupedPath = removeDuplicateConsecutivePoints(path)
if (dedupedPath.length !== path.length) {
path = dedupedPath
}
if (path.length < 3) return path
const newPath: Point[] = [path[0]]
for (let i = 1; i < path.length - 1; i++) {
Expand Down
4 changes: 2 additions & 2 deletions tests/examples/__snapshots__/example02.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions tests/examples/__snapshots__/example13.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading