@eturnity/eturnity_maths 7.48.2 → 7.51.1
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 +26 -13
- package/src/index.js +3 -0
- package/src/intersectionPolygon.js +11 -0
- package/src/objects/Polygon.js +11 -7
- package/src/objects/hydrate.js +4 -2
- 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 +68 -0
- package/src/tests/panelFunctions/dividePanelsIntoAdjacentPatches.spec.js +59 -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 = []
|
|
@@ -848,19 +861,19 @@ export function groupAdjacentObjects(objects) {
|
|
|
848
861
|
if (visited.indexOf(currentObjectIndex) == -1) {
|
|
849
862
|
//if next queued index hasn't been visited, we mark it as visited and we collect all neighbourg to queue
|
|
850
863
|
visited.push(currentObjectIndex)
|
|
851
|
-
let x = objects[currentObjectIndex].
|
|
852
|
-
let y = objects[currentObjectIndex].
|
|
864
|
+
let x = objects[currentObjectIndex].row_index
|
|
865
|
+
let y = objects[currentObjectIndex].col_index
|
|
853
866
|
const left = objects.findIndex(
|
|
854
|
-
(o) => o.
|
|
867
|
+
(o) => o.row_index == x - 1 && o.col_index == y
|
|
855
868
|
)
|
|
856
869
|
const right = objects.findIndex(
|
|
857
|
-
(o) => o.
|
|
870
|
+
(o) => o.row_index == x + 1 && o.col_index == y
|
|
858
871
|
)
|
|
859
872
|
const top = objects.findIndex(
|
|
860
|
-
(o) => o.
|
|
873
|
+
(o) => o.row_index == x && o.col_index == y - 1
|
|
861
874
|
)
|
|
862
875
|
const bottom = objects.findIndex(
|
|
863
|
-
(o) => o.
|
|
876
|
+
(o) => o.row_index == x && o.col_index == y + 1
|
|
864
877
|
)
|
|
865
878
|
if (left != -1 && visited.indexOf(left) == -1) queue.push(left)
|
|
866
879
|
if (right != -1 && visited.indexOf(right) == -1) queue.push(right)
|
|
@@ -887,19 +900,19 @@ export function areAdjacent(objects) {
|
|
|
887
900
|
if (visited.indexOf(currentObjectIndex) == -1) {
|
|
888
901
|
//if next queued index hasn't been visited, we mark it as visited and we collect all neighbourg to queue
|
|
889
902
|
visited.push(currentObjectIndex)
|
|
890
|
-
let x = objects[currentObjectIndex].
|
|
891
|
-
let y = objects[currentObjectIndex].
|
|
903
|
+
let x = objects[currentObjectIndex].row_index
|
|
904
|
+
let y = objects[currentObjectIndex].col_index
|
|
892
905
|
const left = objects.findIndex(
|
|
893
|
-
(o) => o.
|
|
906
|
+
(o) => o.row_index == x - 1 && o.col_index == y
|
|
894
907
|
)
|
|
895
908
|
const right = objects.findIndex(
|
|
896
|
-
(o) => o.
|
|
909
|
+
(o) => o.row_index == x + 1 && o.col_index == y
|
|
897
910
|
)
|
|
898
911
|
const top = objects.findIndex(
|
|
899
|
-
(o) => o.
|
|
912
|
+
(o) => o.row_index == x && o.col_index == y - 1
|
|
900
913
|
)
|
|
901
914
|
const bottom = objects.findIndex(
|
|
902
|
-
(o) => o.
|
|
915
|
+
(o) => o.row_index == x && o.col_index == y + 1
|
|
903
916
|
)
|
|
904
917
|
if (left != -1 && visited.indexOf(left) == -1) queue.push(left)
|
|
905
918
|
if (right != -1 && visited.indexOf(right) == -1) queue.push(right)
|
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,
|
package/src/objects/Polygon.js
CHANGED
|
@@ -38,8 +38,7 @@ export class Polygon {
|
|
|
38
38
|
this.angleOffset = 0
|
|
39
39
|
if (this.layer == 'obstacle') {
|
|
40
40
|
this.isParallel = true
|
|
41
|
-
}
|
|
42
|
-
if (this.layer == 'roof') {
|
|
41
|
+
} else if (this.layer == 'roof') {
|
|
43
42
|
this.moduleFields = []
|
|
44
43
|
}
|
|
45
44
|
}
|
|
@@ -244,7 +243,8 @@ export class Polygon {
|
|
|
244
243
|
this.panels.forEach((p) => {
|
|
245
244
|
modules.push({
|
|
246
245
|
id: p.id,
|
|
247
|
-
|
|
246
|
+
row_index: p.row_index,
|
|
247
|
+
col_index: p.col_index,
|
|
248
248
|
outline: p.outline,
|
|
249
249
|
status: p.status || 'active',
|
|
250
250
|
clipped: p.clipped || false,
|
|
@@ -255,7 +255,8 @@ export class Polygon {
|
|
|
255
255
|
this.userDeactivatedPanels.forEach((p) => {
|
|
256
256
|
modules.push({
|
|
257
257
|
id: p.id,
|
|
258
|
-
|
|
258
|
+
row_index: p.row_index,
|
|
259
|
+
col_index: p.col_index,
|
|
259
260
|
outline: p.outline,
|
|
260
261
|
status: p.status || 'user_deactivated',
|
|
261
262
|
clipped: p.clipped || false,
|
|
@@ -275,10 +276,12 @@ export class Polygon {
|
|
|
275
276
|
extraSerialization.needsOptimisation = this.needsOptimisation
|
|
276
277
|
extraSerialization.priority = this.priority
|
|
277
278
|
} else if (this.layer == 'panel') {
|
|
278
|
-
extraSerialization.
|
|
279
|
+
extraSerialization.row_index = this.row_index
|
|
280
|
+
extraSerialization.col_index = this.col_index
|
|
279
281
|
extraSerialization.moduleField = { id: this.moduleField.id }
|
|
280
282
|
} else if (this.layer == 'user_deactivated_panel') {
|
|
281
|
-
extraSerialization.
|
|
283
|
+
extraSerialization.row_index = this.row_index
|
|
284
|
+
extraSerialization.col_index = this.col_index
|
|
282
285
|
extraSerialization.moduleField = { id: this.moduleField.id }
|
|
283
286
|
}
|
|
284
287
|
return JSON.parse(
|
|
@@ -348,7 +351,8 @@ export class Polygon {
|
|
|
348
351
|
} else if (['panel', 'user_deactivated_panel'].includes(this.layer)) {
|
|
349
352
|
return {
|
|
350
353
|
id: this.id,
|
|
351
|
-
|
|
354
|
+
row_index: this.row_index,
|
|
355
|
+
col_index: this.col_index,
|
|
352
356
|
outline: this.outline.map((v) => [v.x, v.y, v.z]),
|
|
353
357
|
moduleField: {
|
|
354
358
|
id: this.moduleField.id,
|
package/src/objects/hydrate.js
CHANGED
|
@@ -16,10 +16,12 @@ export function hydratePolygon(serializedPolygon) {
|
|
|
16
16
|
polygon.panels = []
|
|
17
17
|
polygon.userDeactivatedPanels = []
|
|
18
18
|
} else if (layer == 'panel') {
|
|
19
|
-
polygon.
|
|
19
|
+
polygon.row_index = serializedPolygon.row_index
|
|
20
|
+
polygon.col_index = serializedPolygon.col_index
|
|
20
21
|
polygon.moduleField = serializedPolygon.moduleField
|
|
21
22
|
} else if (layer == 'user_deactivated_panel') {
|
|
22
|
-
polygon.
|
|
23
|
+
polygon.row_index = serializedPolygon.row_index
|
|
24
|
+
polygon.col_index = serializedPolygon.col_index
|
|
23
25
|
polygon.moduleField = serializedPolygon.moduleField
|
|
24
26
|
}
|
|
25
27
|
return polygon
|
|
@@ -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.index[0] - panelA.index[0]) == 1 &&
|
|
5
|
+
panelB.index[1] == panelA.index[1]
|
|
6
|
+
const areHorizontallyAdjacent =
|
|
7
|
+
Math.abs(panelB.index[1] - panelA.index[1]) == 1 &&
|
|
8
|
+
panelB.index[0] == panelA.index[0]
|
|
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.index[0] - panelA.index[0]) == 1 &&
|
|
15
|
+
Math.abs(panelB.index[1] - panelA.index[1]) == 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
|
+
}
|