@eturnity/eturnity_3d 9.31.1 → 9.37.0-EXP-69.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/README.md CHANGED
@@ -1,18 +1,19 @@
1
- # README #
1
+ # README
2
2
 
3
- This README would normally document whatever steps are necessary to get your application up and running.
3
+ This README would normally document whatever steps are necessary to get your
4
+ application up and running.
4
5
 
5
- ### Setup ###
6
- npm i
7
- npm run dev
8
- In order to preview, run
9
- nodejs server.js
10
- It will serve preview.html file
11
- ### Build Image ###
6
+ ### Setup
12
7
 
13
- * docker build -t registry.digitalocean.com/eturnityhub/eturnity_3d_viewer:6.5.0-master-1 .
8
+ npm i npm run dev In order to preview, run nodejs server.js It will serve
9
+ preview.html file
14
10
 
15
- ### Run Image on Localhost ###
11
+ ### Build Image
16
12
 
17
- * docker run -p 8080:8080 registry.digitalocean.com/eturnityhub/eturnity_3d_viewer:6.5.0-master-1
13
+ - docker build -t
14
+ registry.digitalocean.com/eturnityhub/eturnity_3d_viewer:6.5.0-master-1 .
18
15
 
16
+ ### Run Image on Localhost
17
+
18
+ - docker run -p 8080:8080
19
+ registry.digitalocean.com/eturnityhub/eturnity_3d_viewer:6.5.0-master-1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_3d",
3
3
  "private": false,
4
- "version": "9.31.1",
4
+ "version": "9.37.0-EXP-69.0",
5
5
  "files": [
6
6
  "dist",
7
7
  "src"
@@ -11,12 +11,13 @@
11
11
  "scripts": {
12
12
  "dev": "vite",
13
13
  "build": "vite build",
14
+ "test": "jest",
14
15
  "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src`",
15
16
  "prettier": "prettier --write \"**/*.{js,vue}\"",
16
17
  "merge-remote-master": "node scripts/merge-remote-master.js"
17
18
  },
18
19
  "dependencies": {
19
- "@eturnity/eturnity_maths": "9.31.0",
20
+ "@eturnity/eturnity_maths": "9.37.0-EXP-69.0",
20
21
  "@originjs/vite-plugin-commonjs": "1.0.3",
21
22
  "core-js": "3.30.2",
22
23
  "cors": "2.8.5",
@@ -35,14 +36,19 @@
35
36
  "vuex": "4.1.0"
36
37
  },
37
38
  "devDependencies": {
39
+ "@babel/core": "7.22.0",
40
+ "@babel/preset-env": "7.22.0",
38
41
  "@vitejs/plugin-vue": "2.0.1",
39
42
  "@vue/compiler-sfc": "3.3.4",
40
43
  "@vue/eslint-config-standard": "8.0.1",
44
+ "babel-jest": "29.7.0",
41
45
  "eslint": "8.0.1",
42
46
  "eslint-config-prettier": "8.8.0",
43
47
  "eslint-plugin-prettier": "4.2.1",
44
48
  "eslint-plugin-vue": "9.14.1",
45
49
  "html-webpack-plugin": "3.2.0",
50
+ "jest": "29.7.0",
51
+ "jest-environment-jsdom": "29.7.0",
46
52
  "prettier": "2.8.4",
47
53
  "prettier-plugin-vue": "1.1.6",
48
54
  "vite": "2.7.2",
@@ -120,6 +120,47 @@ export function updateModuleFieldFittingPanels(moduleField) {
120
120
  }
121
121
  }
122
122
 
123
+ // Guarded variant of updateModuleFieldFittingPanels used by the new panel
124
+ // placement engine: preserves the current visibility, tolerates fields without
125
+ // panels/roof/margins, and skips the fitting_panel_area_m2 check.
126
+ export function clipModuleFieldFittingPanels(moduleField) {
127
+ const prevVisible = moduleField?.fittingPanels?.visible
128
+ delete moduleField.fittingPanels
129
+ if (!moduleField?.panels?.length) return
130
+ if (!moduleField?.roof || !moduleField.data?.has_fitting_panels) return
131
+ const innerOutline = moduleField.roof.margins?.innerOutline
132
+ if (!innerOutline?.length) return
133
+
134
+ const outlines = getModuleFieldRoofIntersections(moduleField, innerOutline)
135
+ if (!outlines?.length) return
136
+ const holes = getModuleFieldHoleOutlines(moduleField)
137
+ const polygons = []
138
+
139
+ outlines.forEach((outline) => {
140
+ polygons.push({
141
+ outline,
142
+ // Clean up holes outside of the outline
143
+ holes: holes
144
+ .flatMap((hole) => intersectOutlines(hole, outline))
145
+ .filter(Boolean),
146
+ })
147
+ })
148
+ if (!polygons.length) return
149
+
150
+ const hasCustomTexture = !!moduleField.pvData?.texture_img_url
151
+ const defaultTextureIndex = moduleField.pvData?.module_texture_version_id || 0
152
+ const color = hasCustomTexture
153
+ ? moduleField.pvData?.backsheet_color
154
+ : defaultTextureColorMap[defaultTextureIndex]
155
+ const mappedColor = fittingPanelsColorMap[color || 'default']
156
+
157
+ moduleField.fittingPanels = {
158
+ polygons,
159
+ visible: prevVisible !== false,
160
+ color: mappedColor,
161
+ }
162
+ }
163
+
123
164
  export function getRoofInnerMarginOutline({
124
165
  roof,
125
166
  moduleField,
@@ -0,0 +1,81 @@
1
+ import {
2
+ clipModuleFieldFittingPanels,
3
+ updateModuleFieldFittingPanels,
4
+ } from './UpdateRoofModuleFieldRelations'
5
+
6
+ function rect(x, y, width, height) {
7
+ return [
8
+ { x, y, z: 0 },
9
+ { x: x + width, y, z: 0 },
10
+ { x: x + width, y: y + height, z: 0 },
11
+ { x, y: y + height, z: 0 },
12
+ ]
13
+ }
14
+
15
+ function buildModuleField({ data = { has_fitting_panels: true } } = {}) {
16
+ const roof = {
17
+ id: 'roof-1',
18
+ normalVector: { x: 0, y: 0, z: 1 },
19
+ flatOutline: [{ x: 0, y: 0, z: 0 }],
20
+ margins: { innerOutline: rect(0, 0, 10000, 10000) },
21
+ holes: [],
22
+ moduleFields: [],
23
+ }
24
+ return {
25
+ id: 'mf-1',
26
+ layer: 'moduleField',
27
+ outline: rect(1000, 1000, 4000, 4000),
28
+ roof,
29
+ panels: [{ id: 'panel-1' }],
30
+ data,
31
+ pvData: {},
32
+ priority: 0,
33
+ }
34
+ }
35
+
36
+ describe('clipModuleFieldFittingPanels', () => {
37
+ test('builds fitting panels clipped to the roof inner outline', () => {
38
+ const moduleField = buildModuleField()
39
+ clipModuleFieldFittingPanels(moduleField)
40
+ expect(moduleField.fittingPanels).toBeDefined()
41
+ expect(moduleField.fittingPanels.polygons.length).toBeGreaterThan(0)
42
+ expect(moduleField.fittingPanels.visible).toBe(true)
43
+ })
44
+
45
+ test('preserves an explicitly hidden visibility across rebuilds', () => {
46
+ const moduleField = buildModuleField()
47
+ clipModuleFieldFittingPanels(moduleField)
48
+ moduleField.fittingPanels.visible = false
49
+ clipModuleFieldFittingPanels(moduleField)
50
+ expect(moduleField.fittingPanels.visible).toBe(false)
51
+ })
52
+
53
+ test('removes fitting panels for a field without panels', () => {
54
+ const moduleField = buildModuleField()
55
+ moduleField.fittingPanels = { polygons: [], visible: true }
56
+ moduleField.panels = []
57
+ clipModuleFieldFittingPanels(moduleField)
58
+ expect(moduleField.fittingPanels).toBeUndefined()
59
+ })
60
+
61
+ test('tolerates a field without roof or fitting-panel data', () => {
62
+ const noRoof = buildModuleField()
63
+ noRoof.roof = null
64
+ expect(() => clipModuleFieldFittingPanels(noRoof)).not.toThrow()
65
+ expect(noRoof.fittingPanels).toBeUndefined()
66
+
67
+ const noData = buildModuleField({ data: { has_fitting_panels: false } })
68
+ clipModuleFieldFittingPanels(noData)
69
+ expect(noData.fittingPanels).toBeUndefined()
70
+ })
71
+
72
+ test('does not require fitting_panel_area_m2, unlike the legacy builder', () => {
73
+ const clipped = buildModuleField()
74
+ clipModuleFieldFittingPanels(clipped)
75
+ expect(clipped.fittingPanels).toBeDefined()
76
+
77
+ const legacy = buildModuleField()
78
+ updateModuleFieldFittingPanels(legacy)
79
+ expect(legacy.fittingPanels).toBeUndefined()
80
+ })
81
+ })
@@ -1,6 +1,9 @@
1
1
  //Synchronisation with BE:
2
2
  import {
3
- Polygon,
3
+ Roof,
4
+ Obstacle,
5
+ ModuleField,
6
+ Panel,
4
7
  updateComputedGeometryPolygon,
5
8
  getObstacleMarginOutline,
6
9
  multiplyVector,
@@ -90,7 +93,7 @@ export async function hydratePolygons({
90
93
  const roof_outline = roofResp.roof_outline.map((p) => {
91
94
  return { x: p[0], y: p[1], z: p[2] }
92
95
  })
93
- let roof = new Polygon(roof_outline, 'roof')
96
+ let roof = new Roof(roof_outline)
94
97
  roof.id = roofResp.roof_uuid
95
98
  roof.version = roofResp.version
96
99
  roof.name = roofResp.roof_name
@@ -116,7 +119,7 @@ export async function hydratePolygons({
116
119
  const obstacle_outline = obstacleResp.obstacle_outline.map((p) => {
117
120
  return { x: p[0], y: p[1], z: p[2] }
118
121
  })
119
- let obstacle = new Polygon(obstacle_outline, 'obstacle')
122
+ let obstacle = new Obstacle(obstacle_outline)
120
123
  obstacle.id = obstacleResp.obstacle_uuid
121
124
  obstacle.version = obstacleResp.version
122
125
  obstacle.height = obstacleResp.obstacle_height_above_ground_mm
@@ -145,7 +148,7 @@ export async function hydratePolygons({
145
148
  return { x: p[0], y: p[1], z: p[2] }
146
149
  })
147
150
 
148
- let moduleField = new Polygon(module_field_outline, 'moduleField')
151
+ let moduleField = new ModuleField(module_field_outline)
149
152
  moduleField.id = moduleFieldResp.module_field_uuid
150
153
  moduleField.version = moduleFieldResp.version
151
154
 
@@ -212,7 +215,7 @@ export async function hydratePolygons({
212
215
  })
213
216
 
214
217
  modules.forEach((module) => {
215
- let panel = new Polygon(module.outline, '')
218
+ let panel = new Panel(module.outline)
216
219
  panel.id = module.module_uuid
217
220
  panel.gridIndex = module.gridIndex
218
221
  panel.row_index = module.row_index
@@ -0,0 +1,111 @@
1
+ import { hydratePolygons } from './hydrateData'
2
+ import { Roof, Obstacle, ModuleField, Panel } from '@eturnity/eturnity_maths'
3
+
4
+ const square = (size) => [
5
+ [0, 0, 0],
6
+ [size, 0, 0],
7
+ [size, size, 0],
8
+ [0, size, 0],
9
+ ]
10
+
11
+ const moduleGrid = {
12
+ grid_origin: [0, 0, 0],
13
+ grid_row_vector: [1100, 0, 0],
14
+ grid_column_vector: [0, 1900, 0],
15
+ panel_height_vector: [0, 1800, 0],
16
+ panel_width_vector: [1000, 0, 0],
17
+ modules: [
18
+ {
19
+ module_uuid: 'module-active',
20
+ row_index: 0,
21
+ col_index: 0,
22
+ is_active: true,
23
+ },
24
+ {
25
+ module_uuid: 'module-inactive',
26
+ row_index: 0,
27
+ col_index: 1,
28
+ is_active: false,
29
+ },
30
+ ],
31
+ }
32
+
33
+ async function hydrateFixture() {
34
+ return hydratePolygons({
35
+ roofsResponse: [
36
+ {
37
+ roof_uuid: 'roof-1',
38
+ version: 3,
39
+ roof_outline: square(10000),
40
+ roof_margins_mm: [200, 200, 200, 200],
41
+ roof_slope_degrees: 0,
42
+ roof_orientation_degrees: 180,
43
+ roof_name: 'test roof',
44
+ },
45
+ ],
46
+ obstaclesResponse: [
47
+ {
48
+ obstacle_uuid: 'obstacle-1',
49
+ version: 1,
50
+ obstacle_outline: square(1000),
51
+ obstacle_margins_mm: [100, 100, 100, 100],
52
+ obstacle_height_above_ground_mm: 500,
53
+ obstacle_slope_is_parallel_to_roof: true,
54
+ },
55
+ ],
56
+ moduleFieldsResponse: [
57
+ {
58
+ module_field_uuid: 'mf-1',
59
+ version: 2,
60
+ module_field_outline: square(5000),
61
+ col_spacing_mm: 20,
62
+ row_spacing_mm: 20,
63
+ is_row_spacing_automatic: false,
64
+ offset_percent: 0,
65
+ panel_orientation: 'portrait',
66
+ panel_direction_degrees: 0,
67
+ module_grids: [moduleGrid],
68
+ },
69
+ ],
70
+ })
71
+ }
72
+
73
+ const byLayer = (polygons, layer) => polygons.filter((p) => p.layer == layer)
74
+
75
+ describe('hydratePolygons builds typed polygon instances', () => {
76
+ it('hydrates roofs, obstacles and module fields to their subclasses', async () => {
77
+ const { polygons } = await hydrateFixture()
78
+ const [roof] = byLayer(polygons, 'roof')
79
+ const [obstacle] = byLayer(polygons, 'obstacle')
80
+ const [moduleField] = byLayer(polygons, 'moduleField')
81
+ expect(roof).toBeInstanceOf(Roof)
82
+ expect(obstacle).toBeInstanceOf(Obstacle)
83
+ expect(moduleField).toBeInstanceOf(ModuleField)
84
+ })
85
+
86
+ it('hydrates every grid module as a Panel, with activeness as the layer', async () => {
87
+ const { polygons } = await hydrateFixture()
88
+ const panels = byLayer(polygons, 'panel')
89
+ const userDeactivatedPanels = byLayer(polygons, 'user_deactivated_panel')
90
+ expect(panels).toHaveLength(1)
91
+ expect(panels[0]).toBeInstanceOf(Panel)
92
+ expect(panels[0].id).toBe('module-active')
93
+ expect(userDeactivatedPanels).toHaveLength(1)
94
+ expect(userDeactivatedPanels[0]).toBeInstanceOf(Panel)
95
+ expect(userDeactivatedPanels[0].id).toBe('module-inactive')
96
+ })
97
+
98
+ it('round-trips hydrated polygons through serializeForBEStorage', async () => {
99
+ const { polygons } = await hydrateFixture()
100
+ const [roof] = byLayer(polygons, 'roof')
101
+ const [obstacle] = byLayer(polygons, 'obstacle')
102
+ const roofPayload = roof.serializeForBEStorage()
103
+ expect(roofPayload.roof_uuid).toBe('roof-1')
104
+ expect(roofPayload.roof_outline).toEqual(square(10000))
105
+ expect(roofPayload.roof_margins_mm).toEqual([200, 200, 200, 200])
106
+ const obstaclePayload = obstacle.serializeForBEStorage()
107
+ expect(obstaclePayload.obstacle_uuid).toBe('obstacle-1')
108
+ expect(obstaclePayload.obstacle_height_above_ground_mm).toBe(500)
109
+ expect(obstaclePayload.obstacle_slope_is_parallel_to_roof).toBe(true)
110
+ })
111
+ })