@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
|
|
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
|
-
|
|
8
|
+
npm i npm run dev In order to preview, run nodejs server.js It will serve
|
|
9
|
+
preview.html file
|
|
14
10
|
|
|
15
|
-
###
|
|
11
|
+
### Build Image
|
|
16
12
|
|
|
17
|
-
|
|
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.
|
|
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.
|
|
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
|
+
})
|
package/src/store/hydrateData.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
//Synchronisation with BE:
|
|
2
2
|
import {
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|