@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_maths",
3
- "version": "7.51.0",
3
+ "version": "7.51.2",
4
4
  "author": "Eturnity Team",
5
5
  "main": "src/index.js",
6
6
  "private": false,
@@ -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
@@ -10,3 +10,6 @@ export * from './splitMergePolygons'
10
10
  export * from './miscellaneous'
11
11
  export * from './stats'
12
12
  export * from './spherical'
13
+ export * from './SHPSolver'
14
+ export * from './panelFunctions'
15
+ export * from './stringPatchMatching'
@@ -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
+ })