@eturnity/eturnity_maths 9.13.0 → 9.19.0-EPDM-19634.0

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": "9.13.0",
3
+ "version": "9.19.0-EPDM-19634.0",
4
4
  "author": "Eturnity Team",
5
5
  "main": "src/index.js",
6
6
  "private": false,
@@ -28,8 +28,8 @@
28
28
  "uuid": "9.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@babel/core": "7.21.4",
32
- "@babel/preset-env": "7.21.4",
31
+ "@babel/core": "7.22.0",
32
+ "@babel/preset-env": "7.22.0",
33
33
  "babel-jest": "29.5.0",
34
34
  "husky": "^9.1.5",
35
35
  "@vue/eslint-config-standard": "8.0.1",
package/src/geometry.js CHANGED
@@ -897,7 +897,9 @@ export function projectionOnPlaneFollowingVector(p, n, o, v) {
897
897
  const nv = dotProduct(n, v)
898
898
  if (nv == 0) {
899
899
  console.warn('projection plane and vector are coplanar')
900
- throw new Error('Cannot project: normal vector and direction vector are perpendicular')
900
+ throw new Error(
901
+ 'Cannot project: normal vector and direction vector are perpendicular'
902
+ )
901
903
  }
902
904
  const lambda = -opn / nv
903
905
  const projectedPoint = addVector(p, multiplyVector(lambda, v))
@@ -1086,8 +1088,8 @@ export function getMarginPoint(
1086
1088
  dotProduct(m, m) != 0
1087
1089
  ? addVector(B, m)
1088
1090
  : dotProduct(n, n) != 0
1089
- ? addVector(B, n)
1090
- : B
1091
+ ? addVector(B, n)
1092
+ : B
1091
1093
  } else {
1092
1094
  const mm = dotProduct(m, m)
1093
1095
  const nm = dotProduct(n, m)
@@ -1289,3 +1291,26 @@ export function getIndexesOfBiggestTriangleWithTwoFixedIndexes(
1289
1291
  }
1290
1292
  return maxTriangle
1291
1293
  }
1294
+
1295
+ export function getRotatedRectBounds(width, height, angleDeg) {
1296
+ const rad = (angleDeg * Math.PI) / 180
1297
+ const c = Math.abs(Math.cos(rad))
1298
+ const s = Math.abs(Math.sin(rad))
1299
+ return {
1300
+ width: width * c + height * s,
1301
+ height: width * s + height * c,
1302
+ }
1303
+ }
1304
+ export function getUnrotatedSizeFromAABB(aabbWidth, aabbHeight, angleDeg) {
1305
+ const rad = (angleDeg * Math.PI) / 180
1306
+ const c = Math.abs(Math.cos(rad))
1307
+ const s = Math.abs(Math.sin(rad))
1308
+ const det = c * c - s * s
1309
+ if (Math.abs(det) < 1e-10) {
1310
+ const side = Math.min(aabbWidth, aabbHeight) / Math.sqrt(2)
1311
+ return { width: side, height: side }
1312
+ }
1313
+ const width = (aabbWidth * c - aabbHeight * s) / det
1314
+ const height = (aabbHeight * c - aabbWidth * s) / det
1315
+ return { width: Math.abs(width), height: Math.abs(height) }
1316
+ }
@@ -404,6 +404,7 @@ export class Polygon {
404
404
  } else if (this.layer == 'roof_plan_item') {
405
405
  extraSerialization.colorIndex = this.colorIndex
406
406
  extraSerialization.hasFillColor = this.hasFillColor
407
+ extraSerialization.isClosed = this.isClosed
407
408
  }
408
409
  return JSON.parse(
409
410
  JSON.stringify({ ...baseSerialization, ...extraSerialization })
@@ -78,7 +78,11 @@ export function updateComputedGeometryPolygon(polygon) {
78
78
  return polygon
79
79
  }
80
80
  let newOutline = [...polygon.outline]
81
+ const isOpenRoofPlanItem =
82
+ polygon.layer === 'roof_plan_item' && polygon.isClosed === false
83
+
81
84
  if (
85
+ !isOpenRoofPlanItem &&
82
86
  newOutline.length > 0 &&
83
87
  isAlmostSamePoint2D(newOutline[0], newOutline[newOutline.length - 1], 10)
84
88
  ) {
@@ -90,6 +94,15 @@ export function updateComputedGeometryPolygon(polygon) {
90
94
  }
91
95
  newOutline = calculateValidOutlineFromPolygon(newOutline)
92
96
 
97
+ if (isOpenRoofPlanItem) {
98
+ if (newOutline.length < 2) {
99
+ throw new Error('outline not valid')
100
+ }
101
+ if (newOutline.length < 3) {
102
+ return updateComputedGeometryOpenRoofPlanItemPolygon(polygon, newOutline)
103
+ }
104
+ }
105
+
93
106
  if (newOutline.length < 3) {
94
107
  throw new Error('outline not valid')
95
108
  }
@@ -124,6 +137,9 @@ export function updateComputedGeometryPolygon(polygon) {
124
137
  polygon.direction = direction
125
138
  polygon.area = calculateArea(polygon.flatOutline) / 1000000
126
139
  polygon.isClockwise = isClockWise(polygon.outline)
140
+ if (polygon.layer === 'roof_plan_item' && polygon.isClosed === false) {
141
+ polygon.isClockwise = false
142
+ }
127
143
  polygon = updateMarginOutlinePolygon(polygon)
128
144
  if (polygon.layer == 'moduleField') {
129
145
  let trimedOutline = []
@@ -150,6 +166,27 @@ export function updateComputedGeometryPolygon(polygon) {
150
166
  }
151
167
  return polygon
152
168
  }
169
+
170
+ function updateComputedGeometryOpenRoofPlanItemPolygon(polygon, newOutline) {
171
+ polygon.outline = newOutline
172
+ const normalVector = { x: 0, y: 0, z: 1 }
173
+ const meanPoint = meanVector(newOutline)
174
+ polygon.flatOutline = newOutline.map((p) => ({
175
+ x: p.x,
176
+ y: p.y,
177
+ z: Math.max(0, Math.min(50000, p.z)),
178
+ }))
179
+ polygon.maximumGap = 0
180
+ polygon.isFlat = true
181
+ polygon.normalVector = normalVector
182
+ polygon.meanPoint = meanPoint
183
+ polygon.incline = inclineWithNormalVector(normalVector)
184
+ polygon.direction = directionWithNormalVector(normalVector)
185
+ polygon.area = 0
186
+ polygon.isClockwise = false
187
+ return updateMarginOutlinePolygon(polygon)
188
+ }
189
+
153
190
  export function calculateValidOutlineFromPolygon(outline) {
154
191
  //check if two nodes are same
155
192
  outline = [...outline]
@@ -49,6 +49,9 @@ export function hydratePolygon(serializedPolygon) {
49
49
  polygon.shape = 'polygon'
50
50
  polygon.colorIndex = serializedPolygon.colorIndex
51
51
  polygon.hasFillColor = serializedPolygon.hasFillColor
52
+ if (serializedPolygon.isClosed === false) {
53
+ polygon.isClosed = false
54
+ }
52
55
  }
53
56
  updateComputedGeometryPolygon(polygon)
54
57
  return polygon
@@ -0,0 +1,98 @@
1
+ import { getRotatedRectBounds, getUnrotatedSizeFromAABB } from '../../index'
2
+
3
+ function expectSizeClose(actual, expected, digits = 8) {
4
+ expect(actual.width).toBeCloseTo(expected.width, digits)
5
+ expect(actual.height).toBeCloseTo(expected.height, digits)
6
+ }
7
+
8
+ describe('rectangle rotation helpers', () => {
9
+ describe('getRotatedRectBounds', () => {
10
+ test('returns same size at 0° (and equivalent angles)', () => {
11
+ expectSizeClose(getRotatedRectBounds(120, 45, 0), {
12
+ width: 120,
13
+ height: 45,
14
+ })
15
+ expectSizeClose(getRotatedRectBounds(120, 45, -180), {
16
+ width: 120,
17
+ height: 45,
18
+ })
19
+ expectSizeClose(getRotatedRectBounds(120, 45, 180), {
20
+ width: 120,
21
+ height: 45,
22
+ })
23
+ expectSizeClose(getRotatedRectBounds(120, 45, 360), {
24
+ width: 120,
25
+ height: 45,
26
+ })
27
+ })
28
+
29
+ test('swaps width/height at 90°', () => {
30
+ expectSizeClose(getRotatedRectBounds(120, 45, 90), {
31
+ width: 45,
32
+ height: 120,
33
+ })
34
+ expectSizeClose(getRotatedRectBounds(120, 45, -90), {
35
+ width: 45,
36
+ height: 120,
37
+ })
38
+ })
39
+
40
+ test('matches expected formula for an arbitrary angle', () => {
41
+ const width = 100
42
+ const height = 50
43
+ const angleDeg = 30
44
+ const rad = (angleDeg * Math.PI) / 180
45
+ const c = Math.abs(Math.cos(rad))
46
+ const s = Math.abs(Math.sin(rad))
47
+ expectSizeClose(getRotatedRectBounds(width, height, angleDeg), {
48
+ width: width * c + height * s,
49
+ height: width * s + height * c,
50
+ })
51
+ })
52
+ test('match for thin rectangle for an arbitrary angle', () => {
53
+ const width = 100
54
+ const height = 0.01
55
+ const angleDeg = 30
56
+
57
+ expectSizeClose(
58
+ getRotatedRectBounds(width, height, angleDeg),
59
+ {
60
+ width: (Math.sqrt(3) * 100) / 2,
61
+ height: 50,
62
+ },
63
+ 1
64
+ )
65
+ })
66
+ })
67
+
68
+ describe('getUnrotatedSizeFromAABB', () => {
69
+ test('approximately inverts getRotatedRectBounds (non-degenerate angle)', () => {
70
+ const original = { width: 123.4, height: 56.7 }
71
+ const angleDeg = 27
72
+
73
+ const aabb = getRotatedRectBounds(
74
+ original.width,
75
+ original.height,
76
+ angleDeg
77
+ )
78
+ const unrotated = getUnrotatedSizeFromAABB(
79
+ aabb.width,
80
+ aabb.height,
81
+ angleDeg
82
+ )
83
+
84
+ expectSizeClose(unrotated, original, 6)
85
+ })
86
+
87
+ test('handles the 45° determinant edge-case by returning a square', () => {
88
+ const aabbWidth = 200
89
+ const aabbHeight = 150
90
+ const angleDeg = 45
91
+
92
+ const result = getUnrotatedSizeFromAABB(aabbWidth, aabbHeight, angleDeg)
93
+ const side = Math.min(aabbWidth, aabbHeight) / Math.sqrt(2)
94
+
95
+ expectSizeClose(result, { width: side, height: side }, 10)
96
+ })
97
+ })
98
+ })