@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_maths",
3
- "version": "7.48.2",
3
+ "version": "7.51.1",
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 = []
@@ -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].index[0]
852
- let y = objects[currentObjectIndex].index[1]
864
+ let x = objects[currentObjectIndex].row_index
865
+ let y = objects[currentObjectIndex].col_index
853
866
  const left = objects.findIndex(
854
- (o) => o.index[0] == x - 1 && o.index[1] == y
867
+ (o) => o.row_index == x - 1 && o.col_index == y
855
868
  )
856
869
  const right = objects.findIndex(
857
- (o) => o.index[0] == x + 1 && o.index[1] == y
870
+ (o) => o.row_index == x + 1 && o.col_index == y
858
871
  )
859
872
  const top = objects.findIndex(
860
- (o) => o.index[0] == x && o.index[1] == y - 1
873
+ (o) => o.row_index == x && o.col_index == y - 1
861
874
  )
862
875
  const bottom = objects.findIndex(
863
- (o) => o.index[0] == x && o.index[1] == y + 1
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].index[0]
891
- let y = objects[currentObjectIndex].index[1]
903
+ let x = objects[currentObjectIndex].row_index
904
+ let y = objects[currentObjectIndex].col_index
892
905
  const left = objects.findIndex(
893
- (o) => o.index[0] == x - 1 && o.index[1] == y
906
+ (o) => o.row_index == x - 1 && o.col_index == y
894
907
  )
895
908
  const right = objects.findIndex(
896
- (o) => o.index[0] == x + 1 && o.index[1] == y
909
+ (o) => o.row_index == x + 1 && o.col_index == y
897
910
  )
898
911
  const top = objects.findIndex(
899
- (o) => o.index[0] == x && o.index[1] == y - 1
912
+ (o) => o.row_index == x && o.col_index == y - 1
900
913
  )
901
914
  const bottom = objects.findIndex(
902
- (o) => o.index[0] == x && o.index[1] == y + 1
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
@@ -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,
@@ -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
- index: p.index,
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
- index: p.index,
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.index = this.index
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.index = this.index
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
- index: this.index,
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,
@@ -16,10 +16,12 @@ export function hydratePolygon(serializedPolygon) {
16
16
  polygon.panels = []
17
17
  polygon.userDeactivatedPanels = []
18
18
  } else if (layer == 'panel') {
19
- polygon.index = serializedPolygon.index
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.index = serializedPolygon.index
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
+ }