@eturnity/eturnity_maths 7.51.0 → 7.51.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/SHPSolver.js +342 -0
- package/src/geometry.js +14 -1
- package/src/index.js +3 -0
- package/src/intersectionPolygon.js +11 -0
- package/src/panelFunctions.js +51 -0
- package/src/stringPatchMatching.js +140 -0
- package/src/tests/SHPSolver/SHPSolver.spec.js +71 -0
- package/src/tests/SHPSolver/SHPSolverDebugCase.spec.js +26 -0
- package/src/tests/SHPSolver/scenarios/index.js +17 -0
- package/src/tests/SHPSolver/scenarios/scenario_1.json +58 -0
- package/src/tests/SHPSolver/scenarios/scenario_1_row.json +159 -0
- package/src/tests/SHPSolver/scenarios/scenario_1b.json +35 -0
- package/src/tests/SHPSolver/scenarios/scenario_1b_row.json +51 -0
- package/src/tests/SHPSolver/scenarios/scenario_2_row.json +175 -0
- package/src/tests/SHPSolver/scenarios/scenario_3.json +207 -0
- package/src/tests/SHPSolver/scenarios/scenario_3_simplify.json +151 -0
- package/src/tests/SHPSolver/scenarios/scenario_4.json +198 -0
- package/src/tests/panelFunctions/arePanelsAdjacent.spec.js +78 -0
- package/src/tests/panelFunctions/dividePanelsIntoAdjacentPatches.spec.js +60 -0
- package/src/tests/panelFunctions/stringPatchMatching.spec.js +72 -0
package/package.json
CHANGED
package/src/SHPSolver.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
midPoint,
|
|
3
|
+
substractVector,
|
|
4
|
+
dotProduct,
|
|
5
|
+
vectorLength,
|
|
6
|
+
normalizeVector,
|
|
7
|
+
crossProduct,
|
|
8
|
+
getDegreeVectors,
|
|
9
|
+
areAlmostCollinear,
|
|
10
|
+
isSelfIntersecting,
|
|
11
|
+
isLastPointsInOutlineAtCloseAngle,
|
|
12
|
+
} from './index'
|
|
13
|
+
|
|
14
|
+
class ShortestHamiltonianPathSolver {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.isVerbose = false
|
|
17
|
+
this.DISTANCE_TO_VIRTUAL_NODE = 1
|
|
18
|
+
}
|
|
19
|
+
solve(
|
|
20
|
+
user_nodes,
|
|
21
|
+
{
|
|
22
|
+
maxRecursion = 100000,
|
|
23
|
+
hasVirtualNode = true,
|
|
24
|
+
proximityMatrix = null,
|
|
25
|
+
preventIntersection = false,
|
|
26
|
+
} = {}
|
|
27
|
+
) {
|
|
28
|
+
let sortedNodes = JSON.parse(JSON.stringify(user_nodes))
|
|
29
|
+
let hasSolution = false
|
|
30
|
+
let isOptimal = false
|
|
31
|
+
let indexOrder = Array(user_nodes.length)
|
|
32
|
+
.fill()
|
|
33
|
+
.map((_, i) => i)
|
|
34
|
+
let cost = Infinity
|
|
35
|
+
if (user_nodes.length <= 2) {
|
|
36
|
+
return { sortedNodes, indexOrder, hasSolution, isOptimal, cost }
|
|
37
|
+
}
|
|
38
|
+
// Helper function to calculate distance between two points
|
|
39
|
+
let nodes = JSON.parse(JSON.stringify(user_nodes))
|
|
40
|
+
let adj = this.computeDistanceMatrix(nodes)
|
|
41
|
+
if (hasVirtualNode) {
|
|
42
|
+
nodes = [{ x: -1, y: -1, z: -1 }, ...nodes]
|
|
43
|
+
adj = this.addVirtualNodeToDistanceMatrix(adj)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (proximityMatrix) {
|
|
47
|
+
proximityMatrix = proximityMatrix.slice()
|
|
48
|
+
if (hasVirtualNode) {
|
|
49
|
+
for (let i = 0; i < proximityMatrix.length; i++) {
|
|
50
|
+
let row = proximityMatrix[i].slice()
|
|
51
|
+
row = [1, ...row]
|
|
52
|
+
proximityMatrix[i] = row.slice()
|
|
53
|
+
}
|
|
54
|
+
proximityMatrix.unshift(Array(proximityMatrix.length + 1).fill(1))
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < adj.length; i++) {
|
|
57
|
+
for (let k = 0; k < adj[0].length; k++) {
|
|
58
|
+
adj[i][k] *= proximityMatrix[i][k]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let final_res = Infinity
|
|
63
|
+
let final_path = []
|
|
64
|
+
const resultFromSolver = this.solveFromAdjacencyMatrix(adj, {
|
|
65
|
+
nodes,
|
|
66
|
+
hasVirtualNode,
|
|
67
|
+
maxRecursion,
|
|
68
|
+
proximityMatrix,
|
|
69
|
+
preventIntersection,
|
|
70
|
+
})
|
|
71
|
+
final_path = resultFromSolver.final_path
|
|
72
|
+
final_res = resultFromSolver.final_res
|
|
73
|
+
hasSolution = resultFromSolver.hasSolution
|
|
74
|
+
isOptimal = resultFromSolver.isOptimal
|
|
75
|
+
// Remove virtual node from path and reorder panels
|
|
76
|
+
if (hasSolution) {
|
|
77
|
+
let realPath = final_path
|
|
78
|
+
while (realPath[0] !== 0) {
|
|
79
|
+
realPath.unshift(realPath.pop())
|
|
80
|
+
}
|
|
81
|
+
if (hasVirtualNode) {
|
|
82
|
+
realPath = realPath.filter((i) => i > 0)
|
|
83
|
+
realPath = realPath.map((i) => i - 1)
|
|
84
|
+
final_res -= 2 * this.DISTANCE_TO_VIRTUAL_NODE
|
|
85
|
+
}
|
|
86
|
+
const sortedNodes = realPath.map((i) => user_nodes[i])
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
sortedNodes,
|
|
90
|
+
hasSolution,
|
|
91
|
+
isOptimal,
|
|
92
|
+
indexOrder: realPath,
|
|
93
|
+
cost: final_res,
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
return {
|
|
97
|
+
hasSolution,
|
|
98
|
+
isOptimal,
|
|
99
|
+
sortedNodes: user_nodes,
|
|
100
|
+
indexOrder: Array(user_nodes.length)
|
|
101
|
+
.fill()
|
|
102
|
+
.map((_, i) => i),
|
|
103
|
+
cost: final_res,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
solveFromAdjacencyMatrix(
|
|
108
|
+
adj,
|
|
109
|
+
{
|
|
110
|
+
nodes,
|
|
111
|
+
hasVirtualNode = true,
|
|
112
|
+
maxRecursion = 1000,
|
|
113
|
+
proximityMatrix = null,
|
|
114
|
+
preventIntersection = false,
|
|
115
|
+
} = {}
|
|
116
|
+
) {
|
|
117
|
+
const N = adj.length
|
|
118
|
+
let hasSolution = false
|
|
119
|
+
let isOptimal = false
|
|
120
|
+
let final_path = Array(N).fill(-1)
|
|
121
|
+
let final_res = Infinity
|
|
122
|
+
//precompute final_res
|
|
123
|
+
if (hasVirtualNode) {
|
|
124
|
+
for (let i = 1; i < N - 1; i++) {
|
|
125
|
+
final_res += adj[i][(i + 1) % N]
|
|
126
|
+
}
|
|
127
|
+
final_res += adj[1][N - 1]
|
|
128
|
+
} else {
|
|
129
|
+
for (let i = 0; i < N; i++) {
|
|
130
|
+
final_res += adj[i][(i + 1) % N]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Branch and bound implementation
|
|
134
|
+
let firstMin = Array(N).fill(Number.MAX_SAFE_INTEGER)
|
|
135
|
+
let secondMin = Array(N).fill(Number.MAX_SAFE_INTEGER)
|
|
136
|
+
let thirdMin = Array(N).fill(Number.MAX_SAFE_INTEGER)
|
|
137
|
+
for (let i = 0; i < N; i++) {
|
|
138
|
+
let first_min_i = Number.MAX_SAFE_INTEGER
|
|
139
|
+
let second_min_i = Number.MAX_SAFE_INTEGER
|
|
140
|
+
let third_min_i = Number.MAX_SAFE_INTEGER
|
|
141
|
+
for (let k = 0; k < N; k++) {
|
|
142
|
+
if (i === k && adj[i][k] === 0) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (first_min_i >= adj[i][k]) {
|
|
146
|
+
third_min_i = second_min_i
|
|
147
|
+
second_min_i = first_min_i
|
|
148
|
+
first_min_i = adj[i][k]
|
|
149
|
+
} else if (second_min_i >= adj[i][k]) {
|
|
150
|
+
third_min_i = second_min_i
|
|
151
|
+
second_min_i = adj[i][k]
|
|
152
|
+
} else if (adj[i][k] < third_min_i) {
|
|
153
|
+
third_min_i = adj[i][k]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
firstMin[i] = first_min_i
|
|
157
|
+
secondMin[i] = second_min_i
|
|
158
|
+
thirdMin[i] = third_min_i
|
|
159
|
+
}
|
|
160
|
+
let firstMax = adj.map((row) => {
|
|
161
|
+
return Math.max(...row)
|
|
162
|
+
})
|
|
163
|
+
if (hasVirtualNode) {
|
|
164
|
+
firstMin = secondMin
|
|
165
|
+
secondMin = thirdMin
|
|
166
|
+
}
|
|
167
|
+
const branchAndBound = () => {
|
|
168
|
+
// Initialize stack for DFS with starting state
|
|
169
|
+
let stack = []
|
|
170
|
+
let curr_path = Array(N).fill(-1)
|
|
171
|
+
curr_path[0] = 0 // Start from virtual node
|
|
172
|
+
|
|
173
|
+
// Calculate initial bound
|
|
174
|
+
let curr_bound = hasVirtualNode ? -this.DISTANCE_TO_VIRTUAL_NODE : 0
|
|
175
|
+
for (let i = 0; i < N; i++) {
|
|
176
|
+
curr_bound += (firstMin[i] + secondMin[i]) / 2
|
|
177
|
+
}
|
|
178
|
+
// Initial state: [bound, weight, level, path, visited]
|
|
179
|
+
stack.push([
|
|
180
|
+
curr_bound,
|
|
181
|
+
0,
|
|
182
|
+
1,
|
|
183
|
+
[...curr_path],
|
|
184
|
+
[true, ...Array(N - 1).fill(false)],
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
let recursionCount = 0
|
|
188
|
+
while (stack.length > 0 && recursionCount < maxRecursion) {
|
|
189
|
+
recursionCount++
|
|
190
|
+
// Get next state from stack
|
|
191
|
+
let [curr_bound, curr_weight, level, curr_path, curr_visited] =
|
|
192
|
+
stack.pop()
|
|
193
|
+
// If we've visited all nodes, check if this path is better than current best
|
|
194
|
+
if (level === N) {
|
|
195
|
+
let curr_res = curr_weight + adj[curr_path[level - 1]][curr_path[0]]
|
|
196
|
+
if (curr_res <= final_res) {
|
|
197
|
+
if (
|
|
198
|
+
preventIntersection &&
|
|
199
|
+
this.isPathValid(curr_path, nodes, hasVirtualNode)
|
|
200
|
+
) {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
hasSolution = true
|
|
204
|
+
final_res = curr_res
|
|
205
|
+
final_path = [...curr_path]
|
|
206
|
+
}
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Try each possible next node
|
|
211
|
+
let level_candidates = []
|
|
212
|
+
for (let i = 0; i < N; i++) {
|
|
213
|
+
// Only consider unvisited nodes that are connected
|
|
214
|
+
if (proximityMatrix) {
|
|
215
|
+
if (proximityMatrix[curr_path[level - 1]][i] === 0) {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (adj[curr_path[level - 1]][i] !== 0 && !curr_visited[i]) {
|
|
220
|
+
let new_weight = curr_weight + adj[curr_path[level - 1]][i]
|
|
221
|
+
// Update bound using same logic as before
|
|
222
|
+
let new_bound = curr_bound
|
|
223
|
+
const firstLevelToUpdateBound = hasVirtualNode ? 2 : 1
|
|
224
|
+
const maxDistanceOfFirstLevel = hasVirtualNode
|
|
225
|
+
? firstMax[curr_path[level - 1]]
|
|
226
|
+
: 0
|
|
227
|
+
if (level === firstLevelToUpdateBound) {
|
|
228
|
+
new_bound -= (firstMin[curr_path[level - 1]] + firstMin[i]) / 2
|
|
229
|
+
new_bound += adj[curr_path[level - 1]][i]
|
|
230
|
+
new_bound -= maxDistanceOfFirstLevel
|
|
231
|
+
} else if (level > firstLevelToUpdateBound) {
|
|
232
|
+
new_bound -= (secondMin[curr_path[level - 1]] + firstMin[i]) / 2
|
|
233
|
+
new_bound += adj[curr_path[level - 1]][i]
|
|
234
|
+
}
|
|
235
|
+
// Skip this branch if bound exceeds current best solution
|
|
236
|
+
if (new_bound > final_res) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
// Create new state
|
|
240
|
+
let new_path = [...curr_path]
|
|
241
|
+
new_path[level] = i
|
|
242
|
+
let new_visited = [...curr_visited]
|
|
243
|
+
new_visited[i] = true
|
|
244
|
+
if (
|
|
245
|
+
preventIntersection &&
|
|
246
|
+
this.isPathValid(
|
|
247
|
+
new_path.slice(0, level + 1),
|
|
248
|
+
nodes,
|
|
249
|
+
hasVirtualNode
|
|
250
|
+
)
|
|
251
|
+
) {
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
level_candidates.push([
|
|
256
|
+
new_bound,
|
|
257
|
+
new_weight,
|
|
258
|
+
level + 1,
|
|
259
|
+
new_path,
|
|
260
|
+
new_visited,
|
|
261
|
+
])
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Sort candidates by bound and push to stack in reverse order
|
|
266
|
+
// This ensures we explore the most promising paths first in DFS
|
|
267
|
+
level_candidates.sort((a, b) => b[0] - a[0]) // Reverse sort for stack
|
|
268
|
+
stack.push(...level_candidates)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
isOptimal = true
|
|
272
|
+
}
|
|
273
|
+
// Find best path using branch and bound
|
|
274
|
+
branchAndBound()
|
|
275
|
+
|
|
276
|
+
return { final_path, final_res, hasSolution, isOptimal }
|
|
277
|
+
}
|
|
278
|
+
isPathValid(path, nodes, hasVirtualNode) {
|
|
279
|
+
const outline = path
|
|
280
|
+
.filter((i) => {
|
|
281
|
+
return hasVirtualNode ? i > 0 : i >= 0
|
|
282
|
+
})
|
|
283
|
+
.map((i) => nodes[i])
|
|
284
|
+
if (outline.length > 3) {
|
|
285
|
+
const hasNeedleAngle = isLastPointsInOutlineAtCloseAngle(outline)
|
|
286
|
+
const selfIntersecting = isSelfIntersecting(outline, false)
|
|
287
|
+
return selfIntersecting || hasNeedleAngle
|
|
288
|
+
} else {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
computePathCost(path, nodes, hasVirtualNode) {
|
|
293
|
+
const distance = (p1, p2) => {
|
|
294
|
+
const v = substractVector(p2, p1)
|
|
295
|
+
let distance = vectorLength(v)
|
|
296
|
+
return Math.round(distance)
|
|
297
|
+
}
|
|
298
|
+
const outline = path
|
|
299
|
+
.filter((i) => {
|
|
300
|
+
return hasVirtualNode ? i > 0 : i >= 0
|
|
301
|
+
})
|
|
302
|
+
.map((i) => nodes[i])
|
|
303
|
+
return outline.reduce((acc, cur, index) => {
|
|
304
|
+
const nextIndex = (index + 1) % outline.length
|
|
305
|
+
return acc + distance(cur, outline[nextIndex])
|
|
306
|
+
}, 0)
|
|
307
|
+
}
|
|
308
|
+
computeDistanceMatrix(nodes) {
|
|
309
|
+
const distance = (p1, p2) => {
|
|
310
|
+
const v = substractVector(p2, p1)
|
|
311
|
+
let distance = vectorLength(v)
|
|
312
|
+
return Math.round(distance)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const N = nodes.length
|
|
316
|
+
// Build adjacency matrix with virtual node
|
|
317
|
+
let adj = Array(N)
|
|
318
|
+
.fill()
|
|
319
|
+
.map(() => Array(N).fill(0))
|
|
320
|
+
for (let i = 0; i < N; i++) {
|
|
321
|
+
for (let j = 0; j < N; j++) {
|
|
322
|
+
if (i !== j) {
|
|
323
|
+
adj[i][j] = distance(nodes[i], nodes[j])
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return adj
|
|
328
|
+
}
|
|
329
|
+
addVirtualNodeToDistanceMatrix(adj) {
|
|
330
|
+
// Add new row at beginning
|
|
331
|
+
const N = adj.length
|
|
332
|
+
adj = [Array(N).fill(this.DISTANCE_TO_VIRTUAL_NODE), ...adj.slice()]
|
|
333
|
+
// Add new column at beginning of each row
|
|
334
|
+
for (let i = 0; i < N + 1; i++) {
|
|
335
|
+
adj[i] = [this.DISTANCE_TO_VIRTUAL_NODE, ...adj[i].slice()]
|
|
336
|
+
}
|
|
337
|
+
// Set diagonal to 0
|
|
338
|
+
adj[0][0] = 0
|
|
339
|
+
return adj.slice()
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export const shortestHamiltonianPathSolver = new ShortestHamiltonianPathSolver()
|
package/src/geometry.js
CHANGED
|
@@ -15,7 +15,20 @@ import { Line } from './objects/Line'
|
|
|
15
15
|
import concaveman from './lib/concaveman'
|
|
16
16
|
import cdt2d from 'cdt2d'
|
|
17
17
|
import { isSelfIntersecting } from './intersectionPolygon'
|
|
18
|
-
|
|
18
|
+
export function get3DPolylineLength(outline) {
|
|
19
|
+
return outline.reduce((acc, cur, i) => {
|
|
20
|
+
return (
|
|
21
|
+
acc + get3DDistanceBetweenPoints(cur, outline[(i + 1) % outline.length])
|
|
22
|
+
)
|
|
23
|
+
}, 0)
|
|
24
|
+
}
|
|
25
|
+
export function getPolylineLength(outline) {
|
|
26
|
+
return outline.reduce((acc, cur, i) => {
|
|
27
|
+
return (
|
|
28
|
+
acc + getDistanceBetweenPoints(cur, outline[(i + 1) % outline.length])
|
|
29
|
+
)
|
|
30
|
+
}, 0)
|
|
31
|
+
}
|
|
19
32
|
export function getConcaveOutlines(selectedPanels, onePanelOutline) {
|
|
20
33
|
let buckets = groupAdjacentObjects(selectedPanels)
|
|
21
34
|
const outlines = []
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
getPointInsideOutline,
|
|
3
3
|
isInsidePolygon,
|
|
4
4
|
isSamePoint2D,
|
|
5
|
+
isInsideEdge2D,
|
|
6
|
+
getDegree,
|
|
5
7
|
} from './geometry'
|
|
6
8
|
import {
|
|
7
9
|
getIntersections,
|
|
@@ -85,6 +87,15 @@ export function isSelfIntersecting(outline, isClosePolygon = true) {
|
|
|
85
87
|
}
|
|
86
88
|
return isSelfIntersecting
|
|
87
89
|
}
|
|
90
|
+
export function isLastPointsInOutlineAtCloseAngle(outline) {
|
|
91
|
+
const length = outline.length
|
|
92
|
+
const previousIndex = (length - 1) % length
|
|
93
|
+
const previousP = outline[previousIndex]
|
|
94
|
+
const nextIndex = 0
|
|
95
|
+
const nextP = outline[nextIndex]
|
|
96
|
+
const P = outline[length - 1]
|
|
97
|
+
return getDegree(previousP, P, nextP) == 0
|
|
98
|
+
}
|
|
88
99
|
|
|
89
100
|
export function logicOperationOnPolygons(
|
|
90
101
|
outline1,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function arePanelsAdjacent(panelA, panelB) {
|
|
2
|
+
const areSameModuleField = panelA.moduleField.id == panelB.moduleField.id
|
|
3
|
+
const areVerticalyAdjacent =
|
|
4
|
+
Math.abs(panelB.row_index - panelA.row_index) == 1 &&
|
|
5
|
+
panelB.col_index == panelA.col_index
|
|
6
|
+
const areHorizontallyAdjacent =
|
|
7
|
+
Math.abs(panelB.col_index - panelA.col_index) == 1 &&
|
|
8
|
+
panelB.row_index == panelA.row_index
|
|
9
|
+
return areSameModuleField && (areVerticalyAdjacent || areHorizontallyAdjacent)
|
|
10
|
+
}
|
|
11
|
+
export function arePanelsTouching(panelA, panelB) {
|
|
12
|
+
const areSameModuleField = panelA.moduleField.id == panelB.moduleField.id
|
|
13
|
+
const areTouching =
|
|
14
|
+
Math.abs(panelB.row_index - panelA.row_index) == 1 &&
|
|
15
|
+
Math.abs(panelB.col_index - panelA.col_index) == 1
|
|
16
|
+
return areSameModuleField && areTouching
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function dividePanelsIntoAdjacentPatches(panels) {
|
|
20
|
+
const patches = []
|
|
21
|
+
const visited = new Set()
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < panels.length; i++) {
|
|
24
|
+
if (!visited.has(panels[i])) {
|
|
25
|
+
const currentPatch = []
|
|
26
|
+
const queue = [panels[i]]
|
|
27
|
+
visited.add(panels[i])
|
|
28
|
+
|
|
29
|
+
while (queue.length > 0) {
|
|
30
|
+
const currentPanel = queue.shift()
|
|
31
|
+
currentPatch.push(currentPanel)
|
|
32
|
+
|
|
33
|
+
// Check adjacent panels
|
|
34
|
+
for (let j = 0; j < panels.length; j++) {
|
|
35
|
+
const otherPanel = panels[j]
|
|
36
|
+
if (
|
|
37
|
+
!visited.has(otherPanel) &&
|
|
38
|
+
arePanelsAdjacent(currentPanel, otherPanel)
|
|
39
|
+
) {
|
|
40
|
+
queue.push(otherPanel)
|
|
41
|
+
visited.add(otherPanel)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
patches.push(currentPatch)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return patches
|
|
51
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export function solveStringPatchMatching(stringList, patchList) {
|
|
2
|
+
// Problem Setup
|
|
3
|
+
const n = patchList.length // Number of rows
|
|
4
|
+
const m = stringList.length // Number of columns
|
|
5
|
+
const R = patchList.slice() // Row sum constraints
|
|
6
|
+
const C = stringList.slice() // Column sum constraints
|
|
7
|
+
|
|
8
|
+
// Initialize matrix with zeros
|
|
9
|
+
const matrix = Array.from({ length: n }, () => Array(m).fill(0))
|
|
10
|
+
|
|
11
|
+
// Function to find combinations of columns to satisfy a row sum
|
|
12
|
+
//each row represent a patch, each column represent a string
|
|
13
|
+
//we try to find matches to complete one patch with one or multiple strings
|
|
14
|
+
function findCombinationMatchesForRow(rowSum, colSums) {
|
|
15
|
+
const matches = []
|
|
16
|
+
const colsData = colSums.map((colSum, index) => {
|
|
17
|
+
return { value: colSum, index }
|
|
18
|
+
})
|
|
19
|
+
function findCombinations(
|
|
20
|
+
currentColsData,
|
|
21
|
+
currentSum,
|
|
22
|
+
target,
|
|
23
|
+
availableColsData
|
|
24
|
+
) {
|
|
25
|
+
const stack = [
|
|
26
|
+
{
|
|
27
|
+
currentColsData,
|
|
28
|
+
currentSum,
|
|
29
|
+
availableColsData,
|
|
30
|
+
index: 0,
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
let iterations = 0
|
|
35
|
+
while (stack.length > 0 && iterations < 1000 && matches.length < 5) {
|
|
36
|
+
iterations++
|
|
37
|
+
const current = stack[stack.length - 1]
|
|
38
|
+
|
|
39
|
+
if (current.currentSum === target) {
|
|
40
|
+
matches.push(current.currentColsData.map((col) => col.index))
|
|
41
|
+
stack.pop()
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (current.availableColsData.length === 0) {
|
|
46
|
+
stack.pop()
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Filter once per stack frame
|
|
51
|
+
if (!current.filtered) {
|
|
52
|
+
current.availableColsData = current.availableColsData.filter(
|
|
53
|
+
(colData) => colData.value <= target - current.currentSum
|
|
54
|
+
)
|
|
55
|
+
current.filtered = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (current.index >= current.availableColsData.length) {
|
|
59
|
+
stack.pop()
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const selectedCol = current.availableColsData[current.index]
|
|
64
|
+
current.index++
|
|
65
|
+
|
|
66
|
+
const nextColsData = current.currentColsData.concat(selectedCol)
|
|
67
|
+
const nextSum = current.currentSum + selectedCol.value
|
|
68
|
+
const nextAvailableColsData = current.availableColsData.filter(
|
|
69
|
+
(colData) => colData.index > selectedCol.index
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
stack.push({
|
|
73
|
+
currentColsData: nextColsData,
|
|
74
|
+
currentSum: nextSum,
|
|
75
|
+
availableColsData: nextAvailableColsData,
|
|
76
|
+
index: 0,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
findCombinations([], 0, rowSum, colsData)
|
|
81
|
+
return matches
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Function to assign values to matrix for matched rows and columns
|
|
85
|
+
function assignCombinationPerRowMatches(matches, rowSums, colSums) {
|
|
86
|
+
matches.forEach(({ row, cols }) => {
|
|
87
|
+
if (rowSums[row] && cols.every((col) => colSums[col])) {
|
|
88
|
+
cols.forEach((col) => {
|
|
89
|
+
matrix[row][col] = colSums[col]
|
|
90
|
+
colSums[col] = null // Mark column as completed
|
|
91
|
+
})
|
|
92
|
+
rowSums[row] = null // Mark row as completed
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
// Main greedy decomposition function
|
|
97
|
+
function greedyDecomposition(rowSums, colSums) {
|
|
98
|
+
// Step 1: Try to decompose rows (patch) using column (string) combinations
|
|
99
|
+
const matches = []
|
|
100
|
+
for (let i = 0; i < rowSums.length; i++) {
|
|
101
|
+
if (rowSums[i] !== null) {
|
|
102
|
+
const colCombinations = findCombinationMatchesForRow(
|
|
103
|
+
rowSums[i],
|
|
104
|
+
colSums
|
|
105
|
+
)
|
|
106
|
+
colCombinations.forEach((cols) => {
|
|
107
|
+
matches.push({
|
|
108
|
+
row: i,
|
|
109
|
+
cols,
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//sort the matchs by biggest string first
|
|
115
|
+
matches.sort(
|
|
116
|
+
(a, b) =>
|
|
117
|
+
Math.max(...b.cols.map((col) => colSums[col])) -
|
|
118
|
+
Math.max(...a.cols.map((col) => colSums[col]))
|
|
119
|
+
)
|
|
120
|
+
//sort the matchs by patch in descending order
|
|
121
|
+
matches.sort((a, b) => rowSums[b.row] - rowSums[a.row])
|
|
122
|
+
assignCombinationPerRowMatches(matches, rowSums, colSums)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Run the greedy decomposition algorithm
|
|
126
|
+
greedyDecomposition(R, C)
|
|
127
|
+
|
|
128
|
+
// Output the resulting matrix
|
|
129
|
+
return {
|
|
130
|
+
matrix,
|
|
131
|
+
R,
|
|
132
|
+
C,
|
|
133
|
+
rowsWithSolution: R.map((r, index) => (r == null ? index : null)).filter(
|
|
134
|
+
(r) => r !== null
|
|
135
|
+
),
|
|
136
|
+
colsWithSolution: C.map((c, index) => (c == null ? index : null)).filter(
|
|
137
|
+
(c) => c !== null
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
shortestHamiltonianPathSolver,
|
|
3
|
+
vectorLength,
|
|
4
|
+
substractVector,
|
|
5
|
+
} from '../../index'
|
|
6
|
+
describe('ShortestHamiltonianPathSolver', () => {
|
|
7
|
+
let solver = shortestHamiltonianPathSolver
|
|
8
|
+
it('should return empty array for empty input', () => {
|
|
9
|
+
const result = solver.solve([])
|
|
10
|
+
expect(result.indexOrder).toEqual([])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should handle single node input', () => {
|
|
14
|
+
const nodes = [{ x: 1, y: 1, z: 1 }]
|
|
15
|
+
const result = solver.solve(nodes)
|
|
16
|
+
expect(result.sortedNodes).toEqual(nodes)
|
|
17
|
+
expect(result.indexOrder).toEqual([0])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should find optimal path for two nodes', () => {
|
|
21
|
+
const nodes = [
|
|
22
|
+
{ x: 0, y: 0, z: 0 },
|
|
23
|
+
{ x: 1, y: 1, z: 1 },
|
|
24
|
+
]
|
|
25
|
+
const result = solver.solve(nodes)
|
|
26
|
+
expect(result.sortedNodes).toEqual(nodes)
|
|
27
|
+
expect(result.indexOrder).toEqual([0, 1])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should find optimal path for square', () => {
|
|
31
|
+
const input = [
|
|
32
|
+
{ x: 0, y: 0, z: 0 },
|
|
33
|
+
{ x: 100, y: 100, z: 0 },
|
|
34
|
+
{ x: 0, y: 100, z: 0 },
|
|
35
|
+
{ x: 100, y: 0, z: 0 },
|
|
36
|
+
]
|
|
37
|
+
solver.isVerbose = true
|
|
38
|
+
const expectedIndexOrder = []
|
|
39
|
+
const result = solver.solve(input)
|
|
40
|
+
expect(result.cost).toBe(300)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should find optimal path for line', () => {
|
|
44
|
+
const A = { x: 0, y: 0, z: 0 }
|
|
45
|
+
const B = { x: 0, y: 100, z: 0 }
|
|
46
|
+
const C = { x: 0, y: 200, z: 0 }
|
|
47
|
+
const D = { x: 0, y: 300, z: 0 }
|
|
48
|
+
const E = { x: 0, y: 400, z: 0 }
|
|
49
|
+
|
|
50
|
+
let result = solver.solve([A, B, C, D, E])
|
|
51
|
+
expect(result.cost).toBe(400)
|
|
52
|
+
result = solver.solve([E, B, C, D, A])
|
|
53
|
+
expect(result.cost).toBe(400)
|
|
54
|
+
result = solver.solve([B, C, A, E, D])
|
|
55
|
+
expect(result.cost).toBe(400)
|
|
56
|
+
result = solver.solve([E, D, C, B, A])
|
|
57
|
+
expect(result.cost).toBe(400)
|
|
58
|
+
result = solver.solve([D, E, C, A, B])
|
|
59
|
+
expect(result.cost).toBe(400)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should handle nodes with same coordinates', () => {
|
|
63
|
+
const nodes = [
|
|
64
|
+
{ x: 1, y: 1, z: 1 },
|
|
65
|
+
{ x: 1, y: 1, z: 1 },
|
|
66
|
+
]
|
|
67
|
+
const result = solver.solve(nodes)
|
|
68
|
+
expect(result.sortedNodes.length).toBe(2)
|
|
69
|
+
expect(result.indexOrder.length).toBe(2)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { shortestHamiltonianPathSolver } from '../../index'
|
|
2
|
+
import { scenario_4 } from './scenarios'
|
|
3
|
+
|
|
4
|
+
describe('ShortestHamiltonianPathSolver', () => {
|
|
5
|
+
let solver = shortestHamiltonianPathSolver
|
|
6
|
+
it('should be return shortestHamiltonianPath from real scenario 3', () => {
|
|
7
|
+
// const input = scenario_4.inputData.panels.map((p) => p.center)
|
|
8
|
+
// const N = scenario_4.inputData.panels.length
|
|
9
|
+
// const initialCost = solver.computePathCost(
|
|
10
|
+
// Array(N)
|
|
11
|
+
// .fill()
|
|
12
|
+
// .map((_, i) => i),
|
|
13
|
+
// input,
|
|
14
|
+
// false
|
|
15
|
+
// )
|
|
16
|
+
// const result = solver.solve(input, {
|
|
17
|
+
// hasVirtualNode: true,
|
|
18
|
+
// proximityMatrix: null,
|
|
19
|
+
// preventIntersection: true,
|
|
20
|
+
// })
|
|
21
|
+
// const sortedNodes = result.sortedNodes
|
|
22
|
+
// const areSelfIntersect = solver.isPathValid(result.indexOrder, input, false)
|
|
23
|
+
//console.log('result', initialCost, areSelfIntersect, result)
|
|
24
|
+
//expect(areSelfIntersect).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
})
|