@eturnity/eturnity_3d 8.16.0 → 8.16.2

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.
@@ -2,6 +2,11 @@ import * as THREE from 'three'
2
2
  import { getBufferModuleFieldGeometry } from './geometryHandler'
3
3
  export default {
4
4
  methods: {
5
+ data() {
6
+ return {
7
+ areModuleFieldsVisible: true,
8
+ }
9
+ },
5
10
  renderModuleFields() {
6
11
  this.clearByType('moduleFieldsMeshes')
7
12
  const moduleFieldPolygons = this.polygons.filter((p) =>
@@ -11,8 +16,6 @@ export default {
11
16
  let material
12
17
  if (moduleFieldPolygon.data.number_of_panels_override) {
13
18
  material = this.material.moduleFieldWithPanelOveride
14
- } else if (this.areModuleFieldsVisible) {
15
- material = this.material.moduleField
16
19
  } else {
17
20
  return
18
21
  }
@@ -25,10 +28,17 @@ export default {
25
28
  mesh.userData.layer = moduleFieldPolygon.layer
26
29
  mesh.userData.meshId = moduleFieldPolygon.id
27
30
  mesh.userData.polygonId = moduleFieldPolygon.id
31
+ mesh.visible = this.areModuleFieldsVisible
28
32
  this.scene.add(mesh)
29
33
  this.meshes.moduleFieldsMeshes[moduleFieldPolygon.id] = mesh
30
34
  }
31
35
  })
32
36
  },
37
+ setModuleFieldsVisibility(isVisible) {
38
+ this.areModuleFieldsVisible = isVisible
39
+ Object.values(this.meshes['moduleFieldsMeshes']).forEach(
40
+ (mesh) => (mesh.visible = isVisible)
41
+ )
42
+ },
33
43
  },
34
44
  }
@@ -5,15 +5,13 @@ export default {
5
5
  data() {
6
6
  return {}
7
7
  },
8
- created() {
9
- },
8
+ created() {},
10
9
  computed: {
11
10
  ...mapState({}),
12
11
  },
13
12
  methods: {
14
- renderOverlays() {
13
+ async renderOverlays() {
15
14
  let overlays = OverlayLayer.getItems()
16
-
17
15
  let overlaysIds = overlays.map((o) => o.id)
18
16
  let overlayMeshesIds = Object.keys(this.meshes.overlayMeshes)
19
17
  overlayMeshesIds.forEach((meshId) => {
@@ -22,8 +20,66 @@ export default {
22
20
  this.clearMeshFromScene(mesh, this.scene)
23
21
  }
24
22
  })
23
+ const yearlySunShadingOverlay = overlays.find(
24
+ (o) => o.type == 'yearly_sun_shading'
25
+ )
26
+ let yearlySunShadingMaterial = null
27
+ if (yearlySunShadingOverlay) {
28
+ yearlySunShadingMaterial =
29
+ await yearlySunShadingOverlay.getProjectionMaterial()
30
+ }
25
31
  OverlayLayer.getItems().forEach((overlay) => {
26
- overlay.renderOnThreeJS(this)
32
+ const meshPromise = overlay.renderOnThreeJS(this)
33
+ if (meshPromise) {
34
+ meshPromise.then((mesh) => {
35
+ if (mesh) {
36
+ const texturedRoof = mesh.getObjectByName('textured_roof')
37
+ if (texturedRoof) {
38
+ if (
39
+ yearlySunShadingOverlay &&
40
+ yearlySunShadingOverlay.isActive &&
41
+ yearlySunShadingMaterial
42
+ ) {
43
+ if (!Array.isArray(texturedRoof.material)) {
44
+ texturedRoof.material = [texturedRoof.material]
45
+ }
46
+ texturedRoof.material = texturedRoof.material.filter(
47
+ (m) => m.userData.overlayType != 'yearly_sun_shading'
48
+ )
49
+ texturedRoof.material.push(yearlySunShadingMaterial)
50
+ if (
51
+ texturedRoof.geometry.groups.length !=
52
+ texturedRoof.material.length
53
+ ) {
54
+ texturedRoof.geometry.clearGroups()
55
+ for (let k = 0; k < texturedRoof.material.length; k++) {
56
+ texturedRoof.geometry.addGroup(0, Infinity, k)
57
+ }
58
+ }
59
+
60
+ // Ensure all materials in the array are properly initialized
61
+ texturedRoof.userData.test = true
62
+ this.applyTextureOnMesh(texturedRoof.material, texturedRoof)
63
+ } else {
64
+ if (Array.isArray(texturedRoof.material)) {
65
+ texturedRoof.material = texturedRoof.material.filter(
66
+ (m) => m.userData.overlayType != 'yearly_sun_shading'
67
+ )
68
+ } else {
69
+ texturedRoof.material = [texturedRoof.material]
70
+ texturedRoof.geometry.addGroup(0, Infinity, 0)
71
+ }
72
+ }
73
+ texturedRoof.material.forEach((material) => {
74
+ if (material) {
75
+ material.needsUpdate = true
76
+ }
77
+ })
78
+ }
79
+ }
80
+ })
81
+ }
82
+ //[TODO] Project YearlySunShadingOverlay's material on mesh
27
83
  })
28
84
  },
29
85
  updateOverlayRendering() {
@@ -1,3 +1,4 @@
1
+ import * as THREE from 'three'
1
2
  function cloneMaterialArrayAndAddEmission(material, color) {
2
3
  let newMaterial = material.clone()
3
4
  newMaterial.camera = material.camera
@@ -29,6 +30,7 @@ export default {
29
30
  this.material.roof = [...this.material.roof, ...this.overlayMaterials]
30
31
  this.renderImageOverlayOnBackground()
31
32
  this.applyTextureOnRoofs()
33
+ this.applyTextureOnObstacles()
32
34
  },
33
35
  },
34
36
  }
@@ -120,7 +120,6 @@ export default {
120
120
  this.scene.add(mesh)
121
121
  }
122
122
  }
123
- //roofGeometries.push(this.meshes.roofMeshes[roofPolygon.id].geometry)
124
123
  if (mesh) {
125
124
  this.applyTextureOnMesh(material, mesh)
126
125
  }
@@ -0,0 +1,241 @@
1
+ import * as THREE from 'three'
2
+ import {
3
+ getEdgesForHorizonForShader,
4
+ blurImagePreserveTransparency,
5
+ } from '../shadingMaterial/helper'
6
+ import yearlySunShaderMaterial from './yearlySunShaderMaterial'
7
+ import { getBufferRoofGeometry } from '../geometryHandler'
8
+ import {
9
+ getNormalVectorFromFlatOutline,
10
+ getOptimizedGHI,
11
+ } from '@eturnity/eturnity_maths'
12
+
13
+ export default class ShadingImageGenerator {
14
+ constructor({ lat, lng, dataGrids }) {
15
+ this.lat = lat
16
+ this.lng = lng
17
+ this.dataGrids = dataGrids
18
+ this.maxGHI = getOptimizedGHI({ dataGrids, lat, lng })
19
+ this.scene = new THREE.Scene()
20
+ this.projectionCamera = new THREE.OrthographicCamera(
21
+ -1,
22
+ 1,
23
+ 1,
24
+ -1,
25
+ 0.1,
26
+ 1000
27
+ )
28
+ this.canvas = this.createCanvas()
29
+ this.renderer = new THREE.WebGLRenderer({
30
+ canvas: this.canvas,
31
+ alpha: true,
32
+ })
33
+ this.renderer.setClearColor(0x000000, 0)
34
+ }
35
+ async generateShadingFactors(polygons, edges, normal = null) {
36
+ polygons.forEach((polygon) => {
37
+ let material = new yearlySunShaderMaterial({
38
+ lat: this.lat,
39
+ lng: this.lng,
40
+ dataGrids: this.dataGrids,
41
+ colorfull: false,
42
+ isShadingFactor: true,
43
+ normalVector: normal,
44
+ })
45
+ this.renderPolygon(polygon, edges, material)
46
+ })
47
+ this.updateBox()
48
+ this.updateCanvasFromBoxSize()
49
+ this.updateProjectionCamera()
50
+ const renderTarget = this.getRenderTarget()
51
+ const averageValues = this.getAverageValueFromRenderTarget(
52
+ polygons,
53
+ renderTarget
54
+ )
55
+ this.resetScene()
56
+ return averageValues
57
+ }
58
+ updateBox() {
59
+ const box = new THREE.Box3().setFromObject(this.scene)
60
+ const boxSize = box.getSize(new THREE.Vector3())
61
+ this.boxSize = boxSize
62
+ this.boxCenter = box.getCenter(new THREE.Vector3())
63
+ }
64
+ updateCanvasFromBoxSize() {
65
+ if (!this.boxSize) return
66
+ this.canvasWidth = this.boxSize.x * 10 // 10cm resolution
67
+ this.canvasHeight = this.boxSize.y * 10
68
+ const maxSize = Math.max(this.canvasWidth, this.canvasHeight)
69
+ const CANVAS_MAX_SIZE = 2048
70
+ if (maxSize > CANVAS_MAX_SIZE) {
71
+ this.canvasWidth *= CANVAS_MAX_SIZE / maxSize
72
+ this.canvasHeight *= CANVAS_MAX_SIZE / maxSize
73
+ }
74
+ this.canvasWidth = Math.round(this.canvasWidth)
75
+ this.canvasHeight = Math.round(this.canvasHeight)
76
+ this.renderer.setSize(this.canvasWidth, this.canvasHeight)
77
+ this.canvas.width = this.canvasWidth
78
+ this.canvas.height = this.canvasHeight
79
+ }
80
+ updateProjectionCamera() {
81
+ const left = -this.boxSize.x / 2
82
+ const right = +this.boxSize.x / 2
83
+ const top = +this.boxSize.y / 2
84
+ const bottom = -this.boxSize.y / 2
85
+ this.projectionCamera.position.set(
86
+ this.boxCenter.x,
87
+ this.boxCenter.y,
88
+ this.boxCenter.z + 100
89
+ )
90
+ this.projectionCamera.lookAt(this.boxCenter.x, this.boxCenter.y, 0)
91
+ this.projectionCamera.left = left
92
+ this.projectionCamera.right = right
93
+ this.projectionCamera.top = top
94
+ this.projectionCamera.bottom = bottom
95
+ this.projectionCamera.updateProjectionMatrix()
96
+ }
97
+ renderPolygon(polygon, edges, material) {
98
+ const normalVector = polygon.normalVector
99
+ ? polygon.normalVector
100
+ : getNormalVectorFromFlatOutline(polygon.outline)
101
+ const point = polygon.outline[0]
102
+ const plane = { normalVector, point }
103
+ const edgesForHorizon = getEdgesForHorizonForShader(plane, edges)
104
+ material.updateEdges(edgesForHorizon)
105
+ let geometry = getBufferRoofGeometry(polygon.outline, [])
106
+ this.mesh = new THREE.Mesh(geometry, material)
107
+ this.scene.add(this.mesh)
108
+ }
109
+ getRenderTarget() {
110
+ const renderTarget = new THREE.WebGLRenderTarget(
111
+ this.canvasWidth,
112
+ this.canvasHeight
113
+ )
114
+ this.renderer.setRenderTarget(renderTarget)
115
+ this.renderer.render(this.scene, this.projectionCamera)
116
+
117
+ // Get the URL from the render target
118
+ this.renderer.setRenderTarget(null)
119
+ this.getImage()
120
+ return renderTarget
121
+ }
122
+ getImage() {
123
+ return new Promise((resolve) => {
124
+ // Render the scene
125
+ this.renderer.render(this.scene, this.projectionCamera)
126
+
127
+ // Generate image from canvas
128
+ const imageUrl = this.canvas.toDataURL()
129
+ blurImagePreserveTransparency(imageUrl)
130
+ .then((blurredImageUrl) => {
131
+ resolve(blurredImageUrl)
132
+ })
133
+ .catch((error) => {
134
+ resolve(imageUrl)
135
+ })
136
+ })
137
+ }
138
+ getAverageValueFromRenderTarget(polygons, renderTarget) {
139
+ const pixelBuffer = new Uint8Array(this.canvasWidth * this.canvasHeight * 4)
140
+ this.renderer.readRenderTargetPixels(
141
+ renderTarget,
142
+ 0,
143
+ 0,
144
+ this.canvasWidth,
145
+ this.canvasHeight,
146
+ pixelBuffer
147
+ )
148
+ return polygons.map((polygon) => {
149
+ const pixelCoords = this.convertOutlineToPixelCoords(polygon.outline)
150
+ const pixels = this.getPixelsWithinPolygon(pixelCoords, pixelBuffer)
151
+ return {
152
+ polygonId: polygon.id,
153
+ shadingFactor: this.calculateAverageRedValue(pixels),
154
+ }
155
+ })
156
+ }
157
+
158
+ convertOutlineToPixelCoords(outline) {
159
+ return outline.map((point) => {
160
+ const x =
161
+ ((point.x / 1000 - this.boxCenter.x) / this.boxSize.x + 0.5) *
162
+ this.canvasWidth
163
+ const y =
164
+ ((point.y / 1000 - this.boxCenter.y) / this.boxSize.y + 0.5) *
165
+ this.canvasHeight
166
+ return { x: Math.round(x), y: Math.round(y) }
167
+ })
168
+ }
169
+
170
+ getPixelsWithinPolygon(pixelCoords, pixelBuffer) {
171
+ const pixels = []
172
+ const minX = Math.min(...pixelCoords.map((p) => p.x))
173
+ const maxX = Math.max(...pixelCoords.map((p) => p.x))
174
+ const minY = Math.min(...pixelCoords.map((p) => p.y))
175
+ const maxY = Math.max(...pixelCoords.map((p) => p.y))
176
+
177
+ for (let y = minY; y <= maxY; y++) {
178
+ for (let x = minX; x <= maxX; x++) {
179
+ if (this.isPointInPolygon({ x, y }, pixelCoords)) {
180
+ const index = (y * this.canvasWidth + x) * 4
181
+ pixels.push(pixelBuffer[index])
182
+ pixels.push(pixelBuffer[index + 1])
183
+ pixels.push(pixelBuffer[index + 2])
184
+ pixels.push(pixelBuffer[index + 3])
185
+ }
186
+ }
187
+ }
188
+
189
+ return pixels
190
+ }
191
+
192
+ isPointInPolygon(point, polygon) {
193
+ let inside = false
194
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
195
+ const xi = polygon[i].x,
196
+ yi = polygon[i].y
197
+ const xj = polygon[j].x,
198
+ yj = polygon[j].y
199
+ const intersect =
200
+ yi > point.y !== yj > point.y &&
201
+ point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi
202
+ if (intersect) inside = !inside
203
+ }
204
+ return inside
205
+ }
206
+
207
+ calculateAverageRedValue(pixels) {
208
+ if (pixels.length === 0) return 0
209
+
210
+ let sum = 0
211
+ let count = 0
212
+ for (let i = 0; i < pixels.length; i += 4) {
213
+ const r = pixels[i]
214
+ const a = pixels[i + 3]
215
+ if (a === 255) {
216
+ // Only consider pixels with full opacity
217
+ sum += r
218
+ count++
219
+ }
220
+ }
221
+
222
+ return count > 0 ? sum / count / 255 : 0 // Normalize to 0-1 range
223
+ }
224
+ createCanvas() {
225
+ const canvas = document.createElement('canvas')
226
+ canvas.width = 100
227
+ canvas.height = 100
228
+ canvas.style.position = 'fixed'
229
+ canvas.style.top = '100px'
230
+ canvas.style.left = '50px'
231
+ canvas.style.zIndex = 0
232
+ return canvas
233
+ }
234
+ resetScene() {
235
+ this.scene.remove(this.mesh)
236
+ }
237
+ updateCanvasSize() {
238
+ this.canvas.width = this.canvasWidth
239
+ this.canvas.height = this.canvasHeight
240
+ }
241
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ verticalProjectionOnPlane,
3
+ multiplyVector,
4
+ } from '@eturnity/eturnity_maths'
5
+
6
+ export function getEdgesForHorizonForShader(plane, edges) {
7
+ let roofEdgesForHorizon = edges
8
+ .filter((e) => e.layer == 'roof')
9
+ .map((e) => e.outline)
10
+ let obstacleEdgesForHorizon = edges
11
+ .filter((e) => e.layer == 'obstacle')
12
+ .map((e) => {
13
+ const obstaclePolygon = e.belongsTo[0].polygon
14
+ let obstacleHeight
15
+ if (obstaclePolygon.isParallel) {
16
+ obstacleHeight = parseInt(obstaclePolygon.height || 0)
17
+ return e.outline.map((p) => {
18
+ return { ...p, z: p.z + obstacleHeight }
19
+ })
20
+ } else {
21
+ obstacleHeight = parseInt(
22
+ obstaclePolygon.heightReference + obstaclePolygon.height || 0
23
+ )
24
+ return e.outline.map((p) => {
25
+ return { ...p, z: obstacleHeight }
26
+ })
27
+ }
28
+ })
29
+ let edgesForHorizon = [...roofEdgesForHorizon, ...obstacleEdgesForHorizon]
30
+ .filter((e) =>
31
+ e.some(
32
+ (p) =>
33
+ p.z >
34
+ 0.1 + verticalProjectionOnPlane(p, plane.normalVector, plane.point).z
35
+ )
36
+ )
37
+ .map((e) => e.map((p) => multiplyVector(1 / 1000, p)))
38
+ return edgesForHorizon
39
+ }
40
+ export function blurImagePreserveTransparency(imageUrl, blurRadius = 10) {
41
+ return new Promise((resolve, reject) => {
42
+ const image = new Image()
43
+ image.crossOrigin = 'Anonymous' // Handle CORS if needed
44
+
45
+ image.onload = () => {
46
+ const canvas = document.createElement('canvas')
47
+ const context = canvas.getContext('2d')
48
+
49
+ canvas.width = image.width
50
+ canvas.height = image.height
51
+
52
+ // Draw the original image onto the canvas
53
+ context.drawImage(image, 0, 0)
54
+
55
+ // Get the image data
56
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
57
+ const data = imageData.data
58
+
59
+ // Create a temporary canvas for the blur effect
60
+ const tempCanvas = document.createElement('canvas')
61
+ const tempContext = tempCanvas.getContext('2d')
62
+ tempCanvas.width = canvas.width
63
+ tempCanvas.height = canvas.height
64
+
65
+ // Draw the image onto the temporary canvas
66
+ tempContext.putImageData(imageData, 0, 0)
67
+
68
+ // Apply the blur filter to the temporary canvas
69
+ tempContext.filter = `blur(${blurRadius}px)`
70
+ tempContext.drawImage(tempCanvas, 0, 0)
71
+
72
+ // Get the blurred image data
73
+ const blurredImageData = tempContext.getImageData(
74
+ 0,
75
+ 0,
76
+ tempCanvas.width,
77
+ tempCanvas.height
78
+ )
79
+ const blurredData = blurredImageData.data
80
+
81
+ // Combine the original and blurred data, preserving transparency
82
+ for (let i = 0; i < data.length; i += 4) {
83
+ const isPxNotBlack = data[i] > 0 || data[i + 1] > 0 || data[i + 2] > 0
84
+ if (data[i + 3] > 0 && isPxNotBlack) {
85
+ // If the pixel is not transparent
86
+ data[i] = blurredData[i]
87
+ data[i + 1] = blurredData[i + 1]
88
+ data[i + 2] = blurredData[i + 2]
89
+ }
90
+ }
91
+
92
+ // Put the combined image data back onto the original canvas
93
+ context.putImageData(imageData, 0, 0)
94
+
95
+ // Generate the resulting image URL
96
+ const resultImageUrl = canvas.toDataURL()
97
+
98
+ // Remove canvases after use
99
+ canvas.remove()
100
+ tempCanvas.remove()
101
+
102
+ resolve(resultImageUrl)
103
+ }
104
+ image.onerror = (error) => {
105
+ reject(error)
106
+ }
107
+ image.src = imageUrl
108
+ })
109
+ }