fix: avoid paths corssing each other
This commit is contained in:
parent
17dca6303c
commit
fe361dc877
|
|
@ -127,6 +127,12 @@ function pickLayerNodeType(_layerIndex: number, rng: RNG): MapNodeType {
|
||||||
* Constraints:
|
* Constraints:
|
||||||
* - Each source node gets 1–2 edges to target nodes
|
* - Each source node gets 1–2 edges to target nodes
|
||||||
* - Every target node has at least one incoming edge (no dead ends)
|
* - Every target node has at least one incoming edge (no dead ends)
|
||||||
|
* - Nodes only connect to nearby nodes (by index) to avoid crossing paths
|
||||||
|
*
|
||||||
|
* Strategy to avoid crossings:
|
||||||
|
* - Partition target nodes among source nodes (no overlap)
|
||||||
|
* - Each source can only connect to targets in its assigned partition
|
||||||
|
* - This guarantees no crossings by construction
|
||||||
*/
|
*/
|
||||||
function generateLayerEdges(
|
function generateLayerEdges(
|
||||||
sourceIds: string[],
|
sourceIds: string[],
|
||||||
|
|
@ -139,47 +145,71 @@ function generateLayerEdges(
|
||||||
for (const id of sourceIds) sourceBranches.set(id, 0);
|
for (const id of sourceIds) sourceBranches.set(id, 0);
|
||||||
for (const id of targetIds) targetIncoming.set(id, 0);
|
for (const id of targetIds) targetIncoming.set(id, 0);
|
||||||
|
|
||||||
// --- Pass 1: give each source 1–2 targets, prioritising uncovered targets ---
|
const pickRandom = (arr: string[]): string => arr[rng.nextInt(arr.length)];
|
||||||
|
|
||||||
|
// Partition targets among sources (no overlap)
|
||||||
|
// Each source gets a contiguous range of targets
|
||||||
|
const getTargetRange = (srcIndex: number): { start: number; end: number } => {
|
||||||
|
if (sourceIds.length === 1) {
|
||||||
|
return { start: 0, end: targetIds.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate proportional boundaries
|
||||||
|
const start = Math.floor((srcIndex * targetIds.length) / sourceIds.length);
|
||||||
|
const end = Math.floor(((srcIndex + 1) * targetIds.length) / sourceIds.length);
|
||||||
|
return { start, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Pass 1: give each source 1–2 targets within its partition ---
|
||||||
const uncovered = new Set(targetIds);
|
const uncovered = new Set(targetIds);
|
||||||
|
|
||||||
for (const srcId of sourceIds) {
|
for (let s = 0; s < sourceIds.length; s++) {
|
||||||
const branches = rng.nextInt(2) + 1; // 1 or 2
|
const srcId = sourceIds[s];
|
||||||
|
const range = getTargetRange(s);
|
||||||
|
const availableInPartition = [];
|
||||||
|
|
||||||
for (let b = 0; b < branches; b++) {
|
// Collect available targets in this partition
|
||||||
if (uncovered.size > 0) {
|
for (let t = range.start; t < range.end; t++) {
|
||||||
// Pick a random uncovered target
|
availableInPartition.push(targetIds[t]);
|
||||||
const arr = Array.from(uncovered);
|
}
|
||||||
const idx = rng.nextInt(arr.length);
|
|
||||||
const tgtId = arr[idx];
|
// Decide branches (1 or 2), but limited by available targets
|
||||||
|
const maxBranches = Math.min(2, availableInPartition.length);
|
||||||
|
if (maxBranches === 0) continue;
|
||||||
|
|
||||||
|
const branches = rng.nextInt(maxBranches) + 1;
|
||||||
|
|
||||||
|
// Shuffle and pick
|
||||||
|
const shuffled = [...availableInPartition].sort(() => rng.next() - 0.5);
|
||||||
|
const selected = shuffled.slice(0, branches);
|
||||||
|
|
||||||
|
for (const tgtId of selected) {
|
||||||
nodes.get(srcId)!.childIds.push(tgtId);
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
||||||
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
uncovered.delete(tgtId);
|
uncovered.delete(tgtId);
|
||||||
} else if (sourceBranches.get(srcId)! < 2) {
|
|
||||||
// All targets covered; pick any random target
|
|
||||||
const tgtId = targetIds[rng.nextInt(targetIds.length)];
|
|
||||||
nodes.get(srcId)!.childIds.push(tgtId);
|
|
||||||
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
|
||||||
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Pass 2: cover any remaining uncovered targets ---
|
// --- Pass 2: cover any remaining uncovered targets ---
|
||||||
|
// Since partitions don't overlap, we must assign to the owning source
|
||||||
for (const tgtId of uncovered) {
|
for (const tgtId of uncovered) {
|
||||||
// Find a source that still has room (< 2 branches)
|
const tgtIndex = targetIds.indexOf(tgtId);
|
||||||
const available = sourceIds.filter(id => sourceBranches.get(id)! < 2);
|
|
||||||
if (available.length > 0) {
|
// Find which source partition this target belongs to
|
||||||
const srcId = available[rng.nextInt(available.length)];
|
let owningSource = 0;
|
||||||
|
for (let s = 0; s < sourceIds.length; s++) {
|
||||||
|
const range = getTargetRange(s);
|
||||||
|
if (tgtIndex >= range.start && tgtIndex < range.end) {
|
||||||
|
owningSource = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcId = sourceIds[owningSource];
|
||||||
nodes.get(srcId)!.childIds.push(tgtId);
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
||||||
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
} else {
|
|
||||||
// All sources are at 2 branches; force-add to a random source
|
|
||||||
const srcId = sourceIds[rng.nextInt(sourceIds.length)];
|
|
||||||
nodes.get(srcId)!.childIds.push(tgtId);
|
|
||||||
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,70 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only connect nodes to nearby nodes to avoid crossing paths', () => {
|
||||||
|
const map = generatePointCrawlMap(42);
|
||||||
|
|
||||||
|
// Check each edge between consecutive layers
|
||||||
|
for (let i = 0; i < map.layers.length - 1; i++) {
|
||||||
|
const sourceLayer = map.layers[i];
|
||||||
|
const targetLayer = map.layers[i + 1];
|
||||||
|
|
||||||
|
for (const srcId of sourceLayer.nodeIds) {
|
||||||
|
const srcNode = map.nodes.get(srcId);
|
||||||
|
expect(srcNode).toBeDefined();
|
||||||
|
|
||||||
|
const srcIndex = sourceLayer.nodeIds.indexOf(srcId);
|
||||||
|
|
||||||
|
for (const tgtId of srcNode!.childIds) {
|
||||||
|
const tgtIndex = targetLayer.nodeIds.indexOf(tgtId);
|
||||||
|
|
||||||
|
// Calculate the "scaled" source index to compare with target index
|
||||||
|
// This accounts for layers with different widths
|
||||||
|
const scaledSrcIndex = srcIndex * (targetLayer.nodeIds.length / sourceLayer.nodeIds.length);
|
||||||
|
const distance = Math.abs(tgtIndex - scaledSrcIndex);
|
||||||
|
|
||||||
|
// The distance should be within a reasonable radius
|
||||||
|
// Allow some tolerance for edge cases when covering uncovered targets
|
||||||
|
const maxAllowedDistance = Math.max(2, Math.floor(targetLayer.nodeIds.length / 2));
|
||||||
|
expect(distance).toBeLessThanOrEqual(maxAllowedDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have crossing edges between consecutive layers', () => {
|
||||||
|
const map = generatePointCrawlMap(12345);
|
||||||
|
|
||||||
|
// Check each pair of consecutive layers for crossing edges
|
||||||
|
for (let i = 0; i < map.layers.length - 1; i++) {
|
||||||
|
const sourceLayer = map.layers[i];
|
||||||
|
const targetLayer = map.layers[i + 1];
|
||||||
|
|
||||||
|
// Collect all edges as pairs of indices
|
||||||
|
const edges: Array<{ srcIndex: number; tgtIndex: number }> = [];
|
||||||
|
for (let s = 0; s < sourceLayer.nodeIds.length; s++) {
|
||||||
|
const srcNode = map.nodes.get(sourceLayer.nodeIds[s]);
|
||||||
|
for (const tgtId of srcNode!.childIds) {
|
||||||
|
const t = targetLayer.nodeIds.indexOf(tgtId);
|
||||||
|
edges.push({ srcIndex: s, tgtIndex: t });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for crossings: edge (s1, t1) and (s2, t2) cross if
|
||||||
|
// s1 < s2 but t1 > t2 (or vice versa)
|
||||||
|
for (let e1 = 0; e1 < edges.length; e1++) {
|
||||||
|
for (let e2 = e1 + 1; e2 < edges.length; e2++) {
|
||||||
|
const { srcIndex: s1, tgtIndex: t1 } = edges[e1];
|
||||||
|
const { srcIndex: s2, tgtIndex: t2 } = edges[e2];
|
||||||
|
|
||||||
|
// Skip if they share a source (not a crossing)
|
||||||
|
if (s1 === s2) continue;
|
||||||
|
|
||||||
|
const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2);
|
||||||
|
expect(crosses).toBe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue