@eturnity/eturnity_3d 8.10.1 → 8.16.1
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/Overlay/ImageOverlay.js +16 -139
- package/src/Overlay/Overlay.js +115 -14
- package/src/Overlay/OverlayFactory.js +3 -0
- package/src/Overlay/OverlayLayer.js +6 -0
- package/src/Overlay/ThreeDModelOverlay.js +16 -122
- package/src/Overlay/YearlySunShadingOverlay.js +368 -0
- package/src/helper/render/imageOverlay.js +23 -9
- package/src/helper/render/moduleField.js +12 -2
- package/src/helper/render/overlay.js +60 -5
- package/src/helper/render/projectionMaterial.js +2 -0
- package/src/helper/render/roof.js +0 -1
- package/src/helper/render/shadingMaterial/ShadingImageGenerator.js +241 -0
- package/src/helper/render/shadingMaterial/helper.js +109 -0
- package/src/helper/render/shadingMaterial/yearlySunShaderMaterial.js +389 -0
- package/src/helper/renderMixin.js +17 -0
- package/src/store/hydrateData.js +2 -0
- package/dist/assets/images/panels/longiSolarLR4_60HPH_350M.png +0 -0
- package/dist/assets/images/panels/longiSolarLR4_60HPH_370M.png +0 -0
- package/dist/assets/images/panels/reneSola_Virtus_2_JC320S_24_Bbw copy.png +0 -0
- package/dist/assets/images/panels/reneSola_Virtus_2_JC320S_24_Bbw.png +0 -0
- package/dist/assets/images/panels/uv.png +0 -0
- package/dist/assets/panels/longiSolarLR4_60HPH_350M.png +0 -0
- package/dist/assets/panels/longiSolarLR4_60HPH_370M.png +0 -0
- package/dist/assets/panels/reneSola_Virtus_2_JC320S_24_Bbw.png +0 -0
- package/dist/assets/panels/uv.png +0 -0
- package/dist/assets/theme.js +0 -43
- package/dist/assets/vue.svg +0 -1
- package/dist/main.es.js +0 -43662
- package/dist/main.umd.js +0 -3542
- package/dist/style.css +0 -1
- package/dist/vite.svg +0 -1
|
@@ -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,65 @@ 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.isActive &&
|
|
40
|
+
yearlySunShadingMaterial
|
|
41
|
+
) {
|
|
42
|
+
if (!Array.isArray(texturedRoof.material)) {
|
|
43
|
+
texturedRoof.material = [texturedRoof.material]
|
|
44
|
+
}
|
|
45
|
+
texturedRoof.material = texturedRoof.material.filter(
|
|
46
|
+
(m) => m.userData.overlayType != 'yearly_sun_shading'
|
|
47
|
+
)
|
|
48
|
+
texturedRoof.material.push(yearlySunShadingMaterial)
|
|
49
|
+
if (
|
|
50
|
+
texturedRoof.geometry.groups.length !=
|
|
51
|
+
texturedRoof.material.length
|
|
52
|
+
) {
|
|
53
|
+
texturedRoof.geometry.clearGroups()
|
|
54
|
+
for (let k = 0; k < texturedRoof.material.length; k++) {
|
|
55
|
+
texturedRoof.geometry.addGroup(0, Infinity, k)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Ensure all materials in the array are properly initialized
|
|
60
|
+
texturedRoof.userData.test = true
|
|
61
|
+
this.applyTextureOnMesh(texturedRoof.material, texturedRoof)
|
|
62
|
+
} else {
|
|
63
|
+
if (Array.isArray(texturedRoof.material)) {
|
|
64
|
+
texturedRoof.material = texturedRoof.material.filter(
|
|
65
|
+
(m) => m.userData.overlayType != 'yearly_sun_shading'
|
|
66
|
+
)
|
|
67
|
+
} else {
|
|
68
|
+
texturedRoof.material = [texturedRoof.material]
|
|
69
|
+
texturedRoof.geometry.addGroup(0, Infinity, 0)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
texturedRoof.material.forEach((material) => {
|
|
73
|
+
if (material) {
|
|
74
|
+
material.needsUpdate = true
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
//[TODO] Project YearlySunShadingOverlay's material on mesh
|
|
27
82
|
})
|
|
28
83
|
},
|
|
29
84
|
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
|
}
|
|
@@ -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
|
+
}
|