@eturnity/eturnity_3d 9.40.0 → 9.40.1-EXP-84.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 +2 -2
- package/src/helper/render/edge.js +4 -0
- package/src/helper/render/edge.spec.js +71 -0
- package/src/helper/render/moduleField.js +154 -26
- package/src/helper/render/moduleField.spec.js +195 -0
- package/src/helper/render/panel.js +16 -1
- package/src/helper/render/panel.spec.js +59 -0
- package/src/helper/renderMixin.js +49 -0
- package/src/helper/renderMixin.spec.js +77 -0
- package/src/store/three-d-module.js +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eturnity/eturnity_3d",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "9.40.0",
|
|
4
|
+
"version": "9.40.1-EXP-84.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"src"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"merge-remote-master": "node scripts/merge-remote-master.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@eturnity/eturnity_maths": "9.40.
|
|
20
|
+
"@eturnity/eturnity_maths": "9.40.1",
|
|
21
21
|
"@originjs/vite-plugin-commonjs": "1.0.3",
|
|
22
22
|
"core-js": "3.30.2",
|
|
23
23
|
"cors": "2.8.5",
|
|
@@ -49,6 +49,10 @@ export default {
|
|
|
49
49
|
cylinder.visible = false
|
|
50
50
|
}
|
|
51
51
|
if (['obstacle', 'construction'].includes(edge.layer)) return
|
|
52
|
+
// The new engine draws the module field bound on the 2D canvas instead.
|
|
53
|
+
if (edge.layer === 'moduleField' && this.isNewPanelPlacementEngine) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
52
56
|
if (
|
|
53
57
|
['selectRoof'].includes(this.selectedTool) &&
|
|
54
58
|
edge.layer != 'roof'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import edgeRender from './edge'
|
|
3
|
+
|
|
4
|
+
const { methods } = edgeRender
|
|
5
|
+
|
|
6
|
+
function edge(id, layer, itemId) {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
layer,
|
|
10
|
+
belongsTo: [{ itemId }],
|
|
11
|
+
outline: [
|
|
12
|
+
{ x: 0, y: 0, z: 0 },
|
|
13
|
+
{ x: 1000, y: 0, z: 0 },
|
|
14
|
+
],
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeCtx({ edges, selectedTool, isNewPanelPlacementEngine }) {
|
|
19
|
+
const ctx = {
|
|
20
|
+
edges,
|
|
21
|
+
selectedTool,
|
|
22
|
+
isNewPanelPlacementEngine,
|
|
23
|
+
selectedModuleField: { id: 'mf-1' },
|
|
24
|
+
scene: new THREE.Scene(),
|
|
25
|
+
meshes: { edgeMeshes: {} },
|
|
26
|
+
material: { edge: new THREE.MeshBasicMaterial() },
|
|
27
|
+
clearEdgesRemovedOnes: jest.fn(),
|
|
28
|
+
}
|
|
29
|
+
ctx.setCylinderPosition = methods.setCylinderPosition.bind(ctx)
|
|
30
|
+
ctx.renderEdges = methods.renderEdges.bind(ctx)
|
|
31
|
+
return ctx
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('renderEdges and module field edges', () => {
|
|
35
|
+
const moduleFieldEdge = edge('e-mf', 'moduleField', 'mf-1')
|
|
36
|
+
|
|
37
|
+
it('draws the selected module field outline on the legacy engine', () => {
|
|
38
|
+
const ctx = makeCtx({
|
|
39
|
+
edges: [moduleFieldEdge],
|
|
40
|
+
selectedTool: 'selectModuleField',
|
|
41
|
+
isNewPanelPlacementEngine: false,
|
|
42
|
+
})
|
|
43
|
+
ctx.renderEdges()
|
|
44
|
+
expect(ctx.meshes.edgeMeshes['e-mf']).toBeInstanceOf(THREE.Mesh)
|
|
45
|
+
expect(ctx.scene.children).toHaveLength(1)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('skips module field edges on the new engine, which draws its own bound', () => {
|
|
49
|
+
const ctx = makeCtx({
|
|
50
|
+
edges: [moduleFieldEdge],
|
|
51
|
+
selectedTool: 'selectModuleField',
|
|
52
|
+
isNewPanelPlacementEngine: true,
|
|
53
|
+
})
|
|
54
|
+
ctx.renderEdges()
|
|
55
|
+
expect(ctx.meshes.edgeMeshes['e-mf']).toBeUndefined()
|
|
56
|
+
expect(ctx.scene.children).toHaveLength(0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('keeps drawing roof edges on both engines', () => {
|
|
60
|
+
const roofEdge = edge('e-roof', 'roof', 'roof-1')
|
|
61
|
+
;[false, true].forEach((isNewPanelPlacementEngine) => {
|
|
62
|
+
const ctx = makeCtx({
|
|
63
|
+
edges: [roofEdge],
|
|
64
|
+
selectedTool: 'drawRoof',
|
|
65
|
+
isNewPanelPlacementEngine,
|
|
66
|
+
})
|
|
67
|
+
ctx.renderEdges()
|
|
68
|
+
expect(ctx.meshes.edgeMeshes['e-roof']).toBeInstanceOf(THREE.Mesh)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
getBufferModuleFieldGeometry,
|
|
4
4
|
getFittingPanelsBufferGeometry,
|
|
5
5
|
} from './geometryHandler'
|
|
6
|
+
import { clipModuleFieldFittingPanels } from '../UpdateRoofModuleFieldRelations'
|
|
7
|
+
|
|
8
|
+
const PANEL_OVERRIDE_TYPE = 'moduleFieldsMeshes_panelOverride'
|
|
9
|
+
const FITTING_PANELS_TYPE = 'moduleFieldsMeshes_fittingPanels'
|
|
6
10
|
|
|
7
11
|
export default {
|
|
8
12
|
data() {
|
|
@@ -11,35 +15,150 @@ export default {
|
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
methods: {
|
|
18
|
+
/**
|
|
19
|
+
* Recomputes the fitting panels of every module field from its
|
|
20
|
+
* current panels and re-renders. The new panel placement engine changes
|
|
21
|
+
* panels without a backend re-layout, so nothing else refreshes them.
|
|
22
|
+
*/
|
|
23
|
+
refreshModuleFieldFittingPanels() {
|
|
24
|
+
this.polygons
|
|
25
|
+
.filter((p) => p.layer === 'moduleField' && p.data)
|
|
26
|
+
.forEach((moduleField) => clipModuleFieldFittingPanels(moduleField))
|
|
27
|
+
this.renderModuleFields()
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Incremental: one group per module field survives across renders and its
|
|
31
|
+
* meshes are updated in place, so a live transform never rebuilds the layer.
|
|
32
|
+
*/
|
|
14
33
|
renderModuleFields() {
|
|
15
|
-
this.
|
|
34
|
+
this.clearRemovedModuleFieldGroups()
|
|
16
35
|
const moduleFieldPolygons = this.polygons.filter((p) =>
|
|
17
36
|
['moduleField', 'tmpModuleField'].includes(p.layer)
|
|
18
37
|
)
|
|
19
38
|
moduleFieldPolygons.forEach((moduleFieldPolygon) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
let group = this.meshes.moduleFieldsMeshes[moduleFieldPolygon.id]
|
|
40
|
+
if (!group) {
|
|
41
|
+
group = new THREE.Group()
|
|
42
|
+
group.matrixAutoUpdate = false
|
|
43
|
+
group.userData.type = 'moduleFieldsMeshes'
|
|
44
|
+
group.userData.layer = moduleFieldPolygon.layer
|
|
45
|
+
group.userData.meshId = moduleFieldPolygon.id
|
|
46
|
+
group.userData.polygonId = moduleFieldPolygon.id
|
|
47
|
+
this.scene.add(group)
|
|
48
|
+
this.meshes.moduleFieldsMeshes[moduleFieldPolygon.id] = group
|
|
49
|
+
}
|
|
24
50
|
|
|
25
|
-
|
|
51
|
+
this.updateModuleFieldPanelOverrideMesh(group, moduleFieldPolygon)
|
|
52
|
+
this.updateModuleFieldFittingPanelsMesh(group, moduleFieldPolygon)
|
|
26
53
|
|
|
27
|
-
const meshList = [panelOverrideMesh, fittingPanelsMesh]
|
|
28
|
-
const group = new THREE.Group()
|
|
29
|
-
meshList.forEach((mesh) => {
|
|
30
|
-
if (mesh) group.add(mesh)
|
|
31
|
-
})
|
|
32
|
-
group.matrixAutoUpdate = false
|
|
33
54
|
group.userData.version = moduleFieldPolygon.version
|
|
34
|
-
group.userData.type = 'moduleFieldsMeshes'
|
|
35
|
-
group.userData.layer = moduleFieldPolygon.layer
|
|
36
|
-
group.userData.meshId = moduleFieldPolygon.id
|
|
37
|
-
group.userData.polygonId = moduleFieldPolygon.id
|
|
38
55
|
group.visible = this.areModuleFieldsVisible && this.panelOpacity === 1
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
|
|
57
|
+
if (group.children.length === 0) {
|
|
58
|
+
this.scene.remove(group)
|
|
59
|
+
delete this.meshes.moduleFieldsMeshes[moduleFieldPolygon.id]
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
clearRemovedModuleFieldGroups() {
|
|
64
|
+
const polygonIds = new Set(this.polygons.map((p) => p.id))
|
|
65
|
+
Object.values(this.meshes.moduleFieldsMeshes).forEach((group) => {
|
|
66
|
+
if (!polygonIds.has(group.userData.polygonId)) {
|
|
67
|
+
this.disposeModuleFieldGroupChildren(group)
|
|
68
|
+
this.scene.remove(group)
|
|
69
|
+
delete this.meshes.moduleFieldsMeshes[group.userData.meshId]
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
disposeModuleFieldGroupChildren(group) {
|
|
74
|
+
group.children.forEach((child) => {
|
|
75
|
+
child.geometry?.dispose?.()
|
|
76
|
+
// The override material is shared from materialMixin and must survive.
|
|
77
|
+
if (child.userData.type === FITTING_PANELS_TYPE) {
|
|
78
|
+
child.material?.dispose?.()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
updateModuleFieldPanelOverrideMesh(group, moduleFieldPolygon) {
|
|
83
|
+
let mesh = group.children.find(
|
|
84
|
+
(c) => c.userData.type === PANEL_OVERRIDE_TYPE
|
|
85
|
+
)
|
|
86
|
+
const shouldExist = !!moduleFieldPolygon?.data?.number_of_panels_override
|
|
87
|
+
|
|
88
|
+
if (!shouldExist) {
|
|
89
|
+
if (mesh) {
|
|
90
|
+
group.remove(mesh)
|
|
91
|
+
mesh.geometry?.dispose?.()
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!mesh) {
|
|
97
|
+
mesh = this.renderModuleFieldPanelOverride(moduleFieldPolygon)
|
|
98
|
+
if (mesh) group.add(mesh)
|
|
41
99
|
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (mesh.userData.version !== moduleFieldPolygon.version) {
|
|
103
|
+
const geometry = getBufferModuleFieldGeometry(moduleFieldPolygon)
|
|
104
|
+
if (geometry) {
|
|
105
|
+
mesh.geometry?.dispose?.()
|
|
106
|
+
mesh.geometry = geometry
|
|
107
|
+
mesh.userData.version = moduleFieldPolygon.version
|
|
108
|
+
mesh.updateMatrix()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
updateModuleFieldFittingPanelsMesh(group, moduleFieldPolygon) {
|
|
113
|
+
let mesh = group.children.find(
|
|
114
|
+
(c) => c.userData.type === FITTING_PANELS_TYPE
|
|
115
|
+
)
|
|
116
|
+
const fittingPanels = moduleFieldPolygon?.fittingPanels
|
|
117
|
+
|
|
118
|
+
const removeMesh = () => {
|
|
119
|
+
if (!mesh) return
|
|
120
|
+
group.remove(mesh)
|
|
121
|
+
mesh.geometry?.dispose?.()
|
|
122
|
+
mesh.material?.dispose?.()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!fittingPanels?.polygons?.length) {
|
|
126
|
+
removeMesh()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!mesh) {
|
|
131
|
+
mesh = this.renderModuleFieldFittingPanels(moduleFieldPolygon)
|
|
132
|
+
if (mesh) group.add(mesh)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { polygons, visible, color } = fittingPanels
|
|
137
|
+
const geometries = []
|
|
138
|
+
polygons.forEach((polygon) => {
|
|
139
|
+
const geometry = getFittingPanelsBufferGeometry(
|
|
140
|
+
polygon.outline,
|
|
141
|
+
polygon.holes,
|
|
142
|
+
moduleFieldPolygon
|
|
143
|
+
)
|
|
144
|
+
if (geometry) geometries.push(geometry)
|
|
42
145
|
})
|
|
146
|
+
|
|
147
|
+
if (!geometries.length) {
|
|
148
|
+
removeMesh()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mesh.geometry = this.updateMergedGeometry(geometries, mesh.geometry)
|
|
153
|
+
const isTransparent = color === 'transparent'
|
|
154
|
+
mesh.material.color.set(isTransparent ? '#ffffff' : color)
|
|
155
|
+
mesh.material.transparent = true
|
|
156
|
+
mesh.material.opacity = isTransparent ? 0.2 : 0.8
|
|
157
|
+
mesh.material.needsUpdate = true
|
|
158
|
+
mesh.userData.fittingPanelsVisible = visible
|
|
159
|
+
mesh.userData.version = moduleFieldPolygon.version
|
|
160
|
+
mesh.visible =
|
|
161
|
+
visible && this.areModuleFieldsVisible && this.panelOpacity === 1
|
|
43
162
|
},
|
|
44
163
|
renderModuleFieldPanelOverride(moduleFieldPolygon) {
|
|
45
164
|
if (!moduleFieldPolygon?.data?.number_of_panels_override) return
|
|
@@ -49,7 +168,7 @@ export default {
|
|
|
49
168
|
const mesh = new THREE.Mesh(geometry, material)
|
|
50
169
|
mesh.matrixAutoUpdate = false
|
|
51
170
|
mesh.userData.version = moduleFieldPolygon.version
|
|
52
|
-
mesh.userData.type =
|
|
171
|
+
mesh.userData.type = PANEL_OVERRIDE_TYPE
|
|
53
172
|
mesh.userData.layer = moduleFieldPolygon.layer
|
|
54
173
|
mesh.userData.meshId = moduleFieldPolygon.id
|
|
55
174
|
mesh.userData.polygonId = moduleFieldPolygon.id
|
|
@@ -84,10 +203,13 @@ export default {
|
|
|
84
203
|
})
|
|
85
204
|
|
|
86
205
|
const mesh = new THREE.Mesh(mergedGeometry, material)
|
|
87
|
-
mesh.visible =
|
|
206
|
+
mesh.visible =
|
|
207
|
+
visible && this.areModuleFieldsVisible && this.panelOpacity === 1
|
|
88
208
|
mesh.matrixAutoUpdate = false
|
|
209
|
+
mesh.frustumCulled = false
|
|
210
|
+
mesh.userData.fittingPanelsVisible = visible
|
|
89
211
|
mesh.userData.version = moduleFieldPolygon.version
|
|
90
|
-
mesh.userData.type =
|
|
212
|
+
mesh.userData.type = FITTING_PANELS_TYPE
|
|
91
213
|
mesh.userData.layer = moduleFieldPolygon.layer
|
|
92
214
|
mesh.userData.meshId = moduleFieldPolygon.id
|
|
93
215
|
mesh.userData.polygonId = moduleFieldPolygon.id
|
|
@@ -96,11 +218,17 @@ export default {
|
|
|
96
218
|
},
|
|
97
219
|
updateModuleFieldsVisibility(isVisible) {
|
|
98
220
|
this.areModuleFieldsVisible = isVisible
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
221
|
+
const moduleFieldsVisible =
|
|
222
|
+
this.areModuleFieldsVisible && this.panelOpacity === 1
|
|
223
|
+
Object.values(this.meshes['moduleFieldsMeshes']).forEach((group) => {
|
|
224
|
+
group.visible = moduleFieldsVisible
|
|
225
|
+
group.children.forEach((child) => {
|
|
226
|
+
if (child.userData.type === FITTING_PANELS_TYPE) {
|
|
227
|
+
child.visible =
|
|
228
|
+
moduleFieldsVisible && child.userData.fittingPanelsVisible
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
})
|
|
104
232
|
},
|
|
105
233
|
hideModuleFields() {
|
|
106
234
|
this.updateModuleFieldsVisibility(false)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
jest.mock('./geometryHandler', () => ({
|
|
2
|
+
getBufferModuleFieldGeometry: jest.fn(),
|
|
3
|
+
getFittingPanelsBufferGeometry: jest.fn(),
|
|
4
|
+
}))
|
|
5
|
+
jest.mock('../UpdateRoofModuleFieldRelations', () => ({
|
|
6
|
+
clipModuleFieldFittingPanels: jest.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
import * as THREE from 'three'
|
|
10
|
+
import {
|
|
11
|
+
getBufferModuleFieldGeometry,
|
|
12
|
+
getFittingPanelsBufferGeometry,
|
|
13
|
+
} from './geometryHandler'
|
|
14
|
+
import { clipModuleFieldFittingPanels } from '../UpdateRoofModuleFieldRelations'
|
|
15
|
+
import moduleFieldRender from './moduleField'
|
|
16
|
+
|
|
17
|
+
const { methods } = moduleFieldRender
|
|
18
|
+
|
|
19
|
+
function fittingPanels({ visible = true, color = 'transparent' } = {}) {
|
|
20
|
+
return { visible, color, polygons: [{ outline: [], holes: [] }] }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function moduleField(id, overrides = {}) {
|
|
24
|
+
return { id, layer: 'moduleField', version: 1, data: {}, ...overrides }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeCtx(polygons, overrides = {}) {
|
|
28
|
+
const ctx = {
|
|
29
|
+
polygons,
|
|
30
|
+
scene: new THREE.Scene(),
|
|
31
|
+
meshes: { moduleFieldsMeshes: {} },
|
|
32
|
+
areModuleFieldsVisible: true,
|
|
33
|
+
panelOpacity: 1,
|
|
34
|
+
material: { moduleFieldWithPanelOveride: new THREE.MeshBasicMaterial() },
|
|
35
|
+
createMergedGeometry: () => new THREE.BufferGeometry(),
|
|
36
|
+
updateMergedGeometry: (geometries, existing) => existing,
|
|
37
|
+
}
|
|
38
|
+
Object.entries(methods).forEach(([name, method]) => {
|
|
39
|
+
ctx[name] = method.bind(ctx)
|
|
40
|
+
})
|
|
41
|
+
return Object.assign(ctx, overrides)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.clearAllMocks()
|
|
46
|
+
getFittingPanelsBufferGeometry.mockImplementation(
|
|
47
|
+
() => new THREE.BufferGeometry()
|
|
48
|
+
)
|
|
49
|
+
getBufferModuleFieldGeometry.mockImplementation(
|
|
50
|
+
() => new THREE.BufferGeometry()
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('refreshModuleFieldFittingPanels', () => {
|
|
55
|
+
const roof = { id: 'roof-1', layer: 'roof', data: {} }
|
|
56
|
+
const tmpField = { id: 'tmp-1', layer: 'tmpModuleField', data: {} }
|
|
57
|
+
const fieldWithoutData = { id: 'mf-3', layer: 'moduleField' }
|
|
58
|
+
const first = moduleField('mf-1')
|
|
59
|
+
const second = moduleField(2)
|
|
60
|
+
|
|
61
|
+
it('clips every module field that has data, then re-renders once', () => {
|
|
62
|
+
const ctx = makeCtx([roof, first, tmpField, second, fieldWithoutData], {
|
|
63
|
+
renderModuleFields: jest.fn(),
|
|
64
|
+
})
|
|
65
|
+
ctx.refreshModuleFieldFittingPanels()
|
|
66
|
+
expect(clipModuleFieldFittingPanels.mock.calls.map(([p]) => p)).toEqual([
|
|
67
|
+
first,
|
|
68
|
+
second,
|
|
69
|
+
])
|
|
70
|
+
expect(ctx.renderModuleFields).toHaveBeenCalledTimes(1)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('renderModuleFields', () => {
|
|
75
|
+
it('keeps one group per field across renders and updates it in place', () => {
|
|
76
|
+
const field = moduleField('mf-1', { fittingPanels: fittingPanels() })
|
|
77
|
+
const ctx = makeCtx([field])
|
|
78
|
+
|
|
79
|
+
ctx.renderModuleFields()
|
|
80
|
+
const group = ctx.meshes.moduleFieldsMeshes['mf-1']
|
|
81
|
+
expect(group).toBeInstanceOf(THREE.Group)
|
|
82
|
+
expect(ctx.scene.children).toEqual([group])
|
|
83
|
+
expect(group.children).toHaveLength(1)
|
|
84
|
+
|
|
85
|
+
field.version = 2
|
|
86
|
+
ctx.renderModuleFields()
|
|
87
|
+
expect(ctx.meshes.moduleFieldsMeshes['mf-1']).toBe(group)
|
|
88
|
+
expect(ctx.scene.children).toEqual([group])
|
|
89
|
+
expect(group.children).toHaveLength(1)
|
|
90
|
+
expect(group.userData.version).toBe(2)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('leaves no group behind for a field with nothing to draw', () => {
|
|
94
|
+
const field = moduleField('mf-1', { fittingPanels: fittingPanels() })
|
|
95
|
+
const ctx = makeCtx([field])
|
|
96
|
+
ctx.renderModuleFields()
|
|
97
|
+
|
|
98
|
+
field.fittingPanels = { visible: true, color: 'transparent', polygons: [] }
|
|
99
|
+
ctx.renderModuleFields()
|
|
100
|
+
expect(ctx.meshes.moduleFieldsMeshes).toEqual({})
|
|
101
|
+
expect(ctx.scene.children).toEqual([])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('drops the group of a field that left the polygons', () => {
|
|
105
|
+
const kept = moduleField('mf-1', { fittingPanels: fittingPanels() })
|
|
106
|
+
const removed = moduleField('mf-2', { fittingPanels: fittingPanels() })
|
|
107
|
+
const ctx = makeCtx([kept, removed])
|
|
108
|
+
ctx.renderModuleFields()
|
|
109
|
+
expect(Object.keys(ctx.meshes.moduleFieldsMeshes)).toEqual(['mf-1', 'mf-2'])
|
|
110
|
+
|
|
111
|
+
ctx.polygons = [kept]
|
|
112
|
+
ctx.renderModuleFields()
|
|
113
|
+
expect(Object.keys(ctx.meshes.moduleFieldsMeshes)).toEqual(['mf-1'])
|
|
114
|
+
expect(ctx.scene.children).toEqual([ctx.meshes.moduleFieldsMeshes['mf-1']])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('adds the panel override mesh when the override appears and removes it when it goes', () => {
|
|
118
|
+
const field = moduleField('mf-1', { fittingPanels: fittingPanels() })
|
|
119
|
+
const ctx = makeCtx([field])
|
|
120
|
+
ctx.renderModuleFields()
|
|
121
|
+
const group = ctx.meshes.moduleFieldsMeshes['mf-1']
|
|
122
|
+
const typesOf = () => group.children.map((c) => c.userData.type).sort()
|
|
123
|
+
expect(typesOf()).toEqual(['moduleFieldsMeshes_fittingPanels'])
|
|
124
|
+
|
|
125
|
+
field.data = { number_of_panels_override: 3 }
|
|
126
|
+
field.version = 2
|
|
127
|
+
ctx.renderModuleFields()
|
|
128
|
+
expect(typesOf()).toEqual([
|
|
129
|
+
'moduleFieldsMeshes_fittingPanels',
|
|
130
|
+
'moduleFieldsMeshes_panelOverride',
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
field.data = {}
|
|
134
|
+
field.version = 3
|
|
135
|
+
ctx.renderModuleFields()
|
|
136
|
+
expect(typesOf()).toEqual(['moduleFieldsMeshes_fittingPanels'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Undo hands the field back a version it already rendered at, so a
|
|
140
|
+
// "newer only" check would keep the trimmed slab.
|
|
141
|
+
it('rebuilds the override slab whenever the version differs, not only when it grows', () => {
|
|
142
|
+
const field = moduleField('mf-1', {
|
|
143
|
+
data: { number_of_panels_override: 3 },
|
|
144
|
+
version: 3,
|
|
145
|
+
})
|
|
146
|
+
const ctx = makeCtx([field])
|
|
147
|
+
ctx.renderModuleFields()
|
|
148
|
+
const [overrideMesh] = ctx.meshes.moduleFieldsMeshes['mf-1'].children
|
|
149
|
+
const trimmedGeometry = overrideMesh.geometry
|
|
150
|
+
|
|
151
|
+
const restoredGeometry = new THREE.BufferGeometry()
|
|
152
|
+
getBufferModuleFieldGeometry.mockReturnValueOnce(restoredGeometry)
|
|
153
|
+
field.version = 2
|
|
154
|
+
ctx.renderModuleFields()
|
|
155
|
+
expect(overrideMesh.geometry).toBe(restoredGeometry)
|
|
156
|
+
expect(overrideMesh.geometry).not.toBe(trimmedGeometry)
|
|
157
|
+
expect(overrideMesh.userData.version).toBe(2)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('renderModuleFieldFittingPanels', () => {
|
|
162
|
+
it('never frustum culls the mesh, whose bounding sphere goes stale on moves', () => {
|
|
163
|
+
const field = moduleField('mf-1', { fittingPanels: fittingPanels() })
|
|
164
|
+
const ctx = makeCtx([field])
|
|
165
|
+
ctx.renderModuleFields()
|
|
166
|
+
const [fittingMesh] = ctx.meshes.moduleFieldsMeshes['mf-1'].children
|
|
167
|
+
expect(fittingMesh.frustumCulled).toBe(false)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('updateModuleFieldsVisibility', () => {
|
|
172
|
+
it('hides hidden fitting panels even while the fields are shown', () => {
|
|
173
|
+
const field = moduleField('mf-1', {
|
|
174
|
+
fittingPanels: fittingPanels({ visible: false }),
|
|
175
|
+
})
|
|
176
|
+
const ctx = makeCtx([field])
|
|
177
|
+
ctx.renderModuleFields()
|
|
178
|
+
const group = ctx.meshes.moduleFieldsMeshes['mf-1']
|
|
179
|
+
const [fittingMesh] = group.children
|
|
180
|
+
expect(group.visible).toBe(true)
|
|
181
|
+
expect(fittingMesh.visible).toBe(false)
|
|
182
|
+
|
|
183
|
+
ctx.updateModuleFieldsVisibility(false)
|
|
184
|
+
expect(group.visible).toBe(false)
|
|
185
|
+
|
|
186
|
+
ctx.updateModuleFieldsVisibility(true)
|
|
187
|
+
expect(group.visible).toBe(true)
|
|
188
|
+
expect(fittingMesh.visible).toBe(false)
|
|
189
|
+
|
|
190
|
+
field.fittingPanels.visible = true
|
|
191
|
+
field.version = 2
|
|
192
|
+
ctx.renderModuleFields()
|
|
193
|
+
expect(fittingMesh.visible).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -61,6 +61,15 @@ export default {
|
|
|
61
61
|
this.hidePanels()
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
+
clearMergedPanelMeshes(moduleFieldId) {
|
|
65
|
+
;[this.mergedPanelTopMeshes, this.mergedPanelSideMesh].forEach(
|
|
66
|
+
(registry) => {
|
|
67
|
+
if (!registry[moduleFieldId]) return
|
|
68
|
+
this.clearMeshFromScene(registry[moduleFieldId], this.scene)
|
|
69
|
+
delete registry[moduleFieldId]
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
},
|
|
64
73
|
renderPanelsPerModuleField(moduleField) {
|
|
65
74
|
const panelSideGeometries = []
|
|
66
75
|
const panelTopGeometries = []
|
|
@@ -75,7 +84,13 @@ export default {
|
|
|
75
84
|
existingTopMesh?.userData.panelOpacity !== this.panelOpacity
|
|
76
85
|
const skipRender = existingTopMesh && !isNewerVersion && !opacityChanged
|
|
77
86
|
|
|
78
|
-
if (!panels.length
|
|
87
|
+
if (!panels.length) {
|
|
88
|
+
// The merged meshes outlive the panels otherwise, so the stale panels
|
|
89
|
+
// stay in the scene.
|
|
90
|
+
this.clearMergedPanelMeshes(moduleField.id)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (skipRender) {
|
|
79
94
|
return
|
|
80
95
|
}
|
|
81
96
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
jest.mock('./geometryHandler', () => ({
|
|
2
|
+
getBufferPanelGeometry: jest.fn(),
|
|
3
|
+
updateBufferPanelGeometry: jest.fn(),
|
|
4
|
+
getBufferPanelSideGeometry: jest.fn(),
|
|
5
|
+
updateBufferPanelSideGeometry: jest.fn(),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
import panelRender from './panel'
|
|
9
|
+
|
|
10
|
+
const { methods } = panelRender
|
|
11
|
+
|
|
12
|
+
function makeCtx({ topMesh, sideMesh } = {}) {
|
|
13
|
+
const ctx = {
|
|
14
|
+
scene: { id: 'scene' },
|
|
15
|
+
panelOpacity: 1,
|
|
16
|
+
areUserDeactivatedPanelsVisible: false,
|
|
17
|
+
mergedPanelTopMeshes: topMesh ? { 'mf-1': topMesh } : {},
|
|
18
|
+
mergedPanelSideMesh: sideMesh ? { 'mf-1': sideMesh } : {},
|
|
19
|
+
meshes: { panelsMeshes: {} },
|
|
20
|
+
clearMeshFromScene: jest.fn(),
|
|
21
|
+
getPanelMaterial: jest.fn(),
|
|
22
|
+
}
|
|
23
|
+
ctx.clearMergedPanelMeshes = methods.clearMergedPanelMeshes.bind(ctx)
|
|
24
|
+
return ctx
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('renderPanelsPerModuleField', () => {
|
|
28
|
+
it('drops the merged meshes of a field that lost its last panel', () => {
|
|
29
|
+
const topMesh = { userData: {} }
|
|
30
|
+
const sideMesh = { userData: {} }
|
|
31
|
+
const ctx = makeCtx({ topMesh, sideMesh })
|
|
32
|
+
|
|
33
|
+
methods.renderPanelsPerModuleField.call(ctx, { id: 'mf-1', panels: [] })
|
|
34
|
+
|
|
35
|
+
expect(ctx.clearMeshFromScene).toHaveBeenCalledWith(topMesh, ctx.scene)
|
|
36
|
+
expect(ctx.clearMeshFromScene).toHaveBeenCalledWith(sideMesh, ctx.scene)
|
|
37
|
+
expect(ctx.mergedPanelTopMeshes).toEqual({})
|
|
38
|
+
expect(ctx.mergedPanelSideMesh).toEqual({})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('does nothing for an empty field that never had merged meshes', () => {
|
|
42
|
+
const ctx = makeCtx()
|
|
43
|
+
methods.renderPanelsPerModuleField.call(ctx, { id: 'mf-1', panels: [] })
|
|
44
|
+
expect(ctx.clearMeshFromScene).not.toHaveBeenCalled()
|
|
45
|
+
expect(ctx.getPanelMaterial).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('still skips an unchanged field with panels', () => {
|
|
49
|
+
const topMesh = { userData: { moduleFieldVersion: 4, panelOpacity: 1 } }
|
|
50
|
+
const ctx = makeCtx({ topMesh })
|
|
51
|
+
const field = { id: 'mf-1', version: 4, panels: [{ id: 'p-1' }] }
|
|
52
|
+
|
|
53
|
+
methods.renderPanelsPerModuleField.call(ctx, field)
|
|
54
|
+
|
|
55
|
+
expect(ctx.getPanelMaterial).not.toHaveBeenCalled()
|
|
56
|
+
expect(ctx.clearMeshFromScene).not.toHaveBeenCalled()
|
|
57
|
+
expect(ctx.mergedPanelTopMeshes['mf-1']).toBe(topMesh)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -38,6 +38,10 @@ export default {
|
|
|
38
38
|
selectedMapLayer: (state) => state.threeDModule.selectedMapLayer,
|
|
39
39
|
panelOpacity: (state) => state.threeDModule.panelOpacity,
|
|
40
40
|
pvDataMemo: (state) => state.threeDModule.pvDataMemo,
|
|
41
|
+
// Snapshot the host takes at layout mount; gates the renderer paths that
|
|
42
|
+
// only the new panel placement engine wants.
|
|
43
|
+
isNewPanelPlacementEngine: (state) =>
|
|
44
|
+
state.threeDModule?.isNewPanelPlacementEngineActive === true,
|
|
41
45
|
}),
|
|
42
46
|
...mapGetters({
|
|
43
47
|
selectedTool: 'getSelectedTool',
|
|
@@ -306,6 +310,15 @@ export default {
|
|
|
306
310
|
method: this.renderObstacles,
|
|
307
311
|
arg: [],
|
|
308
312
|
})
|
|
313
|
+
} else if (
|
|
314
|
+
params.layer == 'moduleField' &&
|
|
315
|
+
this.isNewPanelPlacementEngine
|
|
316
|
+
) {
|
|
317
|
+
methodList.push({
|
|
318
|
+
name: 'refreshModuleFieldFittingPanels',
|
|
319
|
+
method: this.refreshModuleFieldFittingPanels,
|
|
320
|
+
arg: [],
|
|
321
|
+
})
|
|
309
322
|
}
|
|
310
323
|
methodList.push({
|
|
311
324
|
name: 'renderRoofsMargin',
|
|
@@ -318,6 +331,25 @@ export default {
|
|
|
318
331
|
arg: [],
|
|
319
332
|
})
|
|
320
333
|
break
|
|
334
|
+
// Legacy emits this too, but its backend re-layout rebuilds the fitting
|
|
335
|
+
// panels afterwards; only the new engine needs the refresh.
|
|
336
|
+
case 'updateModuleFieldOutline':
|
|
337
|
+
if (this.isNewPanelPlacementEngine) {
|
|
338
|
+
methodList.push({
|
|
339
|
+
name: 'refreshModuleFieldFittingPanels',
|
|
340
|
+
method: this.refreshModuleFieldFittingPanels,
|
|
341
|
+
arg: [],
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
break
|
|
345
|
+
// New engine only: the grid changed without a backend re-layout.
|
|
346
|
+
case 'resizeModuleFieldGrid': //params.id
|
|
347
|
+
methodList.push({
|
|
348
|
+
name: 'refreshModuleFieldFittingPanels',
|
|
349
|
+
method: this.refreshModuleFieldFittingPanels,
|
|
350
|
+
arg: [],
|
|
351
|
+
})
|
|
352
|
+
// falls through
|
|
321
353
|
case 'removePanels': //params.panels
|
|
322
354
|
case 'removeModuleField': //params:moduleField
|
|
323
355
|
case 'alignOnSelectedEdge':
|
|
@@ -336,6 +368,23 @@ export default {
|
|
|
336
368
|
arg: [this.areModuleFieldsVisible],
|
|
337
369
|
})
|
|
338
370
|
break
|
|
371
|
+
case 'liveTransformPanels':
|
|
372
|
+
methodList.push({
|
|
373
|
+
name: 'renderPanels',
|
|
374
|
+
method: this.renderPanels,
|
|
375
|
+
arg: [],
|
|
376
|
+
})
|
|
377
|
+
methodList.push({
|
|
378
|
+
name: 'updateModuleFieldsVisibility',
|
|
379
|
+
method: this.updateModuleFieldsVisibility,
|
|
380
|
+
arg: [this.areModuleFieldsVisible],
|
|
381
|
+
})
|
|
382
|
+
methodList.push({
|
|
383
|
+
name: 'renderModuleFields',
|
|
384
|
+
method: this.renderModuleFields,
|
|
385
|
+
arg: [],
|
|
386
|
+
})
|
|
387
|
+
break
|
|
339
388
|
case 'addPointToPolygon': //params.polygon,index
|
|
340
389
|
case 'removeObstacle': //params:obstacle
|
|
341
390
|
case 'removeRoof': //params:roof
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
jest.mock('./render', () => ({}))
|
|
2
|
+
jest.mock('./cameraMixin', () => ({}))
|
|
3
|
+
|
|
4
|
+
import renderMixin from './renderMixin'
|
|
5
|
+
|
|
6
|
+
const { methods, computed } = renderMixin
|
|
7
|
+
|
|
8
|
+
function namesFor(ctx, geometryEvent) {
|
|
9
|
+
return methods.renderingMethodsByEvent
|
|
10
|
+
.call({ areModuleFieldsVisible: true, ...ctx }, geometryEvent)
|
|
11
|
+
.map((m) => m.name)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('isNewPanelPlacementEngine', () => {
|
|
15
|
+
const read = (state) =>
|
|
16
|
+
computed.isNewPanelPlacementEngine.call({ $store: { state } })
|
|
17
|
+
|
|
18
|
+
it('mirrors the snapshot the host store took at layout mount', () => {
|
|
19
|
+
expect(
|
|
20
|
+
read({ threeDModule: { isNewPanelPlacementEngineActive: true } })
|
|
21
|
+
).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('is off without a snapshot or without the module', () => {
|
|
25
|
+
expect(read({ threeDModule: {} })).toBe(false)
|
|
26
|
+
expect(read({})).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('renderingMethodsByEvent and the fitting panel refresh', () => {
|
|
31
|
+
describe('updateOutlineXY on a module field', () => {
|
|
32
|
+
const event = { name: 'updateOutlineXY', params: { layer: 'moduleField' } }
|
|
33
|
+
|
|
34
|
+
it('leaves the legacy engine alone, whose backend re-layout follows', () => {
|
|
35
|
+
const names = namesFor({ isNewPanelPlacementEngine: false }, event)
|
|
36
|
+
expect(names).not.toContain('refreshModuleFieldFittingPanels')
|
|
37
|
+
expect(names).toEqual(['renderRoofsMargin', 'renderTiles'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('refreshes the fitting panels on the new engine', () => {
|
|
41
|
+
const names = namesFor({ isNewPanelPlacementEngine: true }, event)
|
|
42
|
+
expect(names).toEqual([
|
|
43
|
+
'refreshModuleFieldFittingPanels',
|
|
44
|
+
'renderRoofsMargin',
|
|
45
|
+
'renderTiles',
|
|
46
|
+
])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('updateModuleFieldOutline', () => {
|
|
51
|
+
const event = { name: 'updateModuleFieldOutline', params: { id: 'mf-7' } }
|
|
52
|
+
|
|
53
|
+
it('stays a no-op on the legacy engine', () => {
|
|
54
|
+
expect(namesFor({ isNewPanelPlacementEngine: false }, event)).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('refreshes every field on the new engine, since refreshRendering keeps one call per method name', () => {
|
|
58
|
+
const ctx = {
|
|
59
|
+
isNewPanelPlacementEngine: true,
|
|
60
|
+
fullRenderNeeded: false,
|
|
61
|
+
geometryEventsToDoWithoutFull: [
|
|
62
|
+
event,
|
|
63
|
+
{ name: 'updateModuleFieldOutline', params: { id: 'mf-8' } },
|
|
64
|
+
],
|
|
65
|
+
renderingMethodsByEvent: methods.renderingMethodsByEvent,
|
|
66
|
+
renderMethods: jest.fn(),
|
|
67
|
+
render: jest.fn(),
|
|
68
|
+
}
|
|
69
|
+
methods.refreshRendering.call(ctx)
|
|
70
|
+
const [methodList] = ctx.renderMethods.mock.calls[0]
|
|
71
|
+
expect(methodList.map((m) => m.name)).toEqual([
|
|
72
|
+
'refreshModuleFieldFittingPanels',
|
|
73
|
+
])
|
|
74
|
+
expect(methodList[0].arg).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|