@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.
Files changed (32) hide show
  1. package/package.json +2 -2
  2. package/src/Overlay/ImageOverlay.js +16 -139
  3. package/src/Overlay/Overlay.js +115 -14
  4. package/src/Overlay/OverlayFactory.js +3 -0
  5. package/src/Overlay/OverlayLayer.js +6 -0
  6. package/src/Overlay/ThreeDModelOverlay.js +16 -122
  7. package/src/Overlay/YearlySunShadingOverlay.js +368 -0
  8. package/src/helper/render/imageOverlay.js +23 -9
  9. package/src/helper/render/moduleField.js +12 -2
  10. package/src/helper/render/overlay.js +60 -5
  11. package/src/helper/render/projectionMaterial.js +2 -0
  12. package/src/helper/render/roof.js +0 -1
  13. package/src/helper/render/shadingMaterial/ShadingImageGenerator.js +241 -0
  14. package/src/helper/render/shadingMaterial/helper.js +109 -0
  15. package/src/helper/render/shadingMaterial/yearlySunShaderMaterial.js +389 -0
  16. package/src/helper/renderMixin.js +17 -0
  17. package/src/store/hydrateData.js +2 -0
  18. package/dist/assets/images/panels/longiSolarLR4_60HPH_350M.png +0 -0
  19. package/dist/assets/images/panels/longiSolarLR4_60HPH_370M.png +0 -0
  20. package/dist/assets/images/panels/reneSola_Virtus_2_JC320S_24_Bbw copy.png +0 -0
  21. package/dist/assets/images/panels/reneSola_Virtus_2_JC320S_24_Bbw.png +0 -0
  22. package/dist/assets/images/panels/uv.png +0 -0
  23. package/dist/assets/panels/longiSolarLR4_60HPH_350M.png +0 -0
  24. package/dist/assets/panels/longiSolarLR4_60HPH_370M.png +0 -0
  25. package/dist/assets/panels/reneSola_Virtus_2_JC320S_24_Bbw.png +0 -0
  26. package/dist/assets/panels/uv.png +0 -0
  27. package/dist/assets/theme.js +0 -43
  28. package/dist/assets/vue.svg +0 -1
  29. package/dist/main.es.js +0 -43662
  30. package/dist/main.umd.js +0 -3542
  31. package/dist/style.css +0 -1
  32. package/dist/vite.svg +0 -1
@@ -0,0 +1,389 @@
1
+ import * as THREE from 'three'
2
+ import { getOptimizedGHI } from '@eturnity/eturnity_maths'
3
+ export default class yearlySunShaderMaterial extends THREE.MeshPhysicalMaterial {
4
+ constructor(parameters) {
5
+ const {
6
+ lat,
7
+ lng,
8
+ dataGrids,
9
+ normalVector,
10
+ colorfull = true,
11
+ // Default grayscale colormap
12
+ colorMap = [
13
+ { value: 0, color: { r: 255, g: 255, b: 255 } },
14
+ { value: 1.0, color: { r: 0, g: 0, b: 0 } },
15
+ ],
16
+ isShadingFactor = false,
17
+ edges = [],
18
+ ...otherParameters
19
+ } = parameters
20
+ super(otherParameters)
21
+ this.normalVector = normalVector
22
+ this.MAX_EDGES_LENGTH = 500
23
+ this.needsUpdate = true
24
+ this.horizonResolution = this.dataGrids?.horizon?.length || 36
25
+ this.latitude = lat
26
+ this.longitude = lng
27
+ this.colorfull = colorfull
28
+ this.isShadingFactor = isShadingFactor
29
+ this.dataGrids = dataGrids
30
+ this.edges = edges
31
+ this.colorMap = [...colorMap].sort((a, b) => a.value - b.value)
32
+ let maxDNI = 0
33
+ this.dataGrids.dni_grid.forEach((dni_row) =>
34
+ dni_row.forEach((dni) => {
35
+ if (dni > maxDNI) {
36
+ maxDNI = dni
37
+ }
38
+ })
39
+ )
40
+ let maxDHI = 0
41
+ this.dataGrids.dhi_grid.forEach((dhi_row) =>
42
+ dhi_row.forEach((dhi) => (maxDHI = maxDHI > dhi ? maxDHI : dhi))
43
+ )
44
+ // Normalize DNI values using maxDNI
45
+ const normalizedDNIGrid = this.dataGrids.dni_grid.map((row) =>
46
+ row.map((dni) => dni / maxDNI)
47
+ )
48
+ const normalizedDHIGrid = this.dataGrids.dhi_grid.map((row) =>
49
+ row.map((dhi) => dhi / maxDHI)
50
+ )
51
+ const dniTexture = new THREE.DataTexture(
52
+ new Float32Array(normalizedDNIGrid.flat()),
53
+ this.dataGrids.dni_grid[0].length,
54
+ this.dataGrids.dni_grid.length,
55
+ THREE.RedFormat,
56
+ THREE.FloatType
57
+ )
58
+
59
+ const dhiTexture = new THREE.DataTexture(
60
+ new Float32Array(normalizedDHIGrid.flat()),
61
+ this.dataGrids.dhi_grid[0].length,
62
+ this.dataGrids.dhi_grid.length,
63
+ THREE.RedFormat,
64
+ THREE.FloatType
65
+ )
66
+
67
+ const horizonTexture = new THREE.DataTexture(
68
+ new Float32Array(this.dataGrids.horizon),
69
+ this.horizonResolution,
70
+ 1,
71
+ THREE.RedFormat,
72
+ THREE.FloatType
73
+ )
74
+
75
+ dniTexture.needsUpdate = true
76
+ dhiTexture.needsUpdate = true
77
+ horizonTexture.needsUpdate = true
78
+
79
+ // Compute highest GHI
80
+ let highestGHI = getOptimizedGHI({
81
+ dataGrids: this.dataGrids,
82
+ lat,
83
+ lng,
84
+ })
85
+
86
+ const flattenedEdges = this.edges.flat().flatMap((v) => [v.x, v.y, v.z])
87
+
88
+ const stopValues = this.colorMap.map((stop) => stop.value)
89
+ const colorStops = this.colorMap
90
+ .map((stop) => {
91
+ const { r, g, b } = stop.color
92
+ return [r / 255, g / 255, b / 255]
93
+ })
94
+ .flat()
95
+
96
+ this.uniforms = {
97
+ gridsDNI: { value: dniTexture },
98
+ gridsDHI: { value: dhiTexture },
99
+ maxDNI: { value: maxDNI },
100
+ maxDHI: { value: maxDHI },
101
+ highestGHI: { value: highestGHI },
102
+ hasNormalVector: { value: this.normalVector ? 1 : 0 },
103
+ normalVector: {
104
+ value: this.normalVector ? this.normalVector : [0, 0, 0],
105
+ },
106
+ edges: { value: new Float32Array(flattenedEdges) },
107
+ horizon: { value: horizonTexture },
108
+ edgesLength: { value: this.edges.length },
109
+ colorfull: { value: this.colorfull ? 1 : 0 },
110
+ stopValues: { value: new Float32Array(stopValues) },
111
+ colorStops: {
112
+ value: new Float32Array(colorStops),
113
+ },
114
+ stopCount: { value: this.colorMap.length },
115
+ isShadingFactor: { value: this.isShadingFactor ? 1 : 0 },
116
+ }
117
+ this.count = 0
118
+ this.onBeforeCompile = this.onBeforeCompile.bind(this)
119
+ }
120
+ updateEdges(newEdges) {
121
+ if (newEdges.length == 0) {
122
+ newEdges = [
123
+ [
124
+ { x: 0, y: 0, z: 0 },
125
+ { x: 0, y: 0, z: 0 },
126
+ ],
127
+ ]
128
+ }
129
+ if (this.MAX_EDGES_LENGTH < newEdges.length) {
130
+ console.warn(
131
+ `Reduced edges from ${this.edges.length} to ${newEdges.length} to fit within MAX_EDGES_LENGTH (${this.MAX_EDGES_LENGTH})`
132
+ )
133
+ }
134
+ // Ensure we don't exceed MAX_EDGES_LENGTH
135
+ if (newEdges.length > this.MAX_EDGES_LENGTH) {
136
+ newEdges = newEdges.slice(0, this.MAX_EDGES_LENGTH)
137
+ }
138
+ this.edges = [...newEdges]
139
+ const flattenedEdges = newEdges.flat().flatMap((v) => [v.x, v.y, v.z])
140
+ this.uniforms.edges = { value: new Float32Array(flattenedEdges) }
141
+ this.uniforms.edgesLength = { value: newEdges.length }
142
+ this.needsUpdate = true
143
+ // Manually trigger recompilation
144
+ }
145
+ onBeforeCompile(shader) {
146
+ this.needsUpdate = true
147
+ Object.assign(this.uniforms, shader.uniforms)
148
+ shader.uniforms = this.uniforms
149
+ const flattenedEdges = this.edges.flat().flatMap((v) => [v.x, v.y, v.z])
150
+ shader.uniforms.edges = {
151
+ value: new Float32Array(flattenedEdges),
152
+ }
153
+ shader.uniforms.edgesLength = { value: this.edges.length }
154
+ this.compiledEdgesLength = this.edges.length
155
+ const vertexShader = `
156
+ varying vec4 vPosition;
157
+ varying vec2 screenPosition;
158
+ varying vec3 vNormal;
159
+ uniform int hasNormalVector;
160
+ uniform vec3 normalVector;
161
+ void main() {
162
+ vPosition = modelMatrix * vec4(position, 1.0);
163
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
164
+ screenPosition = gl_Position.xy;
165
+ vNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
166
+ if(hasNormalVector == 1){
167
+ vNormal = normalize(normalVector);
168
+ }
169
+
170
+ }
171
+ `
172
+ const fragmentShader = `
173
+ uniform sampler2D gridsDNI;
174
+ uniform sampler2D gridsDHI;
175
+ uniform float maxDNI;
176
+ uniform float maxDHI;
177
+ uniform float highestGHI;
178
+ uniform vec3 edges[${this.MAX_EDGES_LENGTH * 2}];
179
+ uniform sampler2D horizon;
180
+ uniform int edgesLength;
181
+ uniform int colorfull;
182
+ uniform int isShadingFactor;
183
+ uniform int stopCount;
184
+ uniform float stopValues[${this.colorMap.length}];
185
+ uniform vec3 colorStops[${this.colorMap.length}];
186
+ varying vec3 vNormal;
187
+ varying vec2 screenPosition;
188
+ varying vec4 vPosition;
189
+ int horizonResolution = ${this.horizonResolution};
190
+ float GROUND_ALBEDO = 0.2;
191
+
192
+ // Function to convert Cartesian to spherical coordinates
193
+ vec3 cartesianToSpherical(vec3 cartesian) {
194
+ float r = length(cartesian);
195
+ float cotheta = acos(cartesian.z / r);
196
+ float theta = 3.14159 / 2.0 - cotheta;
197
+ float phi = atan(cartesian.x, cartesian.y);
198
+ phi = mod(phi + 6.28318, 6.28318);
199
+ return vec3(r, theta, phi);
200
+ }
201
+ int getHorizonIndexFromPhiAngle(float phi) {
202
+ // Ensure phi is within [0, 2π]
203
+ phi = mod(phi, 6.28318);
204
+
205
+ // Convert phi to degrees and scale to horizon resolution
206
+ float scaledPhi = (phi / (6.28318)) * float(horizonResolution);
207
+
208
+ // Convert to integer and ensure it's within valid range
209
+ return clamp(int(scaledPhi), 0, horizonResolution - 1);
210
+ }
211
+ // Function to calculate the horizon from edges with resolution
212
+ void calculateHorizon(
213
+ out float horizonThetas[${this.horizonResolution}],
214
+ out float horizonMountainThetas[${this.horizonResolution}]
215
+ ) {
216
+ for (int i = 0; i < ${this.horizonResolution}; i++) {
217
+ float horizonValue = texture2D(horizon, vec2(float(i) / float(horizonResolution), 0.0)).r;
218
+ horizonThetas[i] = horizonValue;
219
+ horizonMountainThetas[i] = horizonValue;
220
+ }
221
+
222
+ for (int i = 0; i < ${this.MAX_EDGES_LENGTH}; i++) {
223
+ if (i > edgesLength) break;
224
+ vec3 edgeStart = cartesianToSpherical(edges[i*2] - vPosition.xyz);
225
+ vec3 edgeEnd = cartesianToSpherical(edges[i*2+1] - vPosition.xyz);
226
+ vec3 eStart=edges[i*2] - vPosition.xyz;
227
+ vec3 eEnd=edges[i*2 + 1] - vPosition.xyz;
228
+ bool flipEdges = false;
229
+ float phiDiff = mod(edgeEnd.z - edgeStart.z + 6.28318, 6.28318);
230
+ if (phiDiff > 3.14159) {
231
+ flipEdges = true;
232
+ }
233
+ if(flipEdges){
234
+ vec3 edgeTmp = edgeStart;
235
+ edgeStart = edgeEnd;
236
+ edgeEnd = edgeTmp;
237
+ vec3 eTmp = eStart;
238
+ eStart = eEnd;
239
+ eEnd = eTmp;
240
+ }
241
+ int startPhiIndex = getHorizonIndexFromPhiAngle(edgeStart.z);
242
+ int endPhiIndex = getHorizonIndexFromPhiAngle(edgeEnd.z);
243
+ if(startPhiIndex > endPhiIndex){
244
+ endPhiIndex += horizonResolution;
245
+ }
246
+ for (int j = startPhiIndex; j <= endPhiIndex; j++) {
247
+ int index = j % horizonResolution;
248
+ float phi = 2.0 * 3.14159 * float(index) / float(horizonResolution);
249
+ vec3 u = eEnd - eStart;
250
+
251
+ float denominator=u.x*cos(phi)-u.y*sin(phi);
252
+ if(denominator==0.0){
253
+ continue;
254
+ }
255
+ float numerator=eStart.y*sin(phi)-eStart.x*cos(phi);
256
+ float t = numerator/denominator;
257
+ if(t<0.0 || t>1.0){
258
+ continue;
259
+ }
260
+ // Interpolate the Cartesian position
261
+ vec3 interpolatedCartesian = mix(eStart, eEnd, t);
262
+ // Convert interpolated Cartesian to spherical
263
+ vec3 interpolatedSpherical = cartesianToSpherical(interpolatedCartesian);
264
+ // Use the theta (y) component of the spherical coordinates
265
+ float interpolatedTheta = interpolatedSpherical.y;
266
+ horizonThetas[index] = max(horizonThetas[index], interpolatedTheta);
267
+ }
268
+ }
269
+ }
270
+
271
+ vec3 mapValueToColor(float value) {
272
+ for (int i = 0; i < stopCount; i++) {
273
+ float stop1 = stopValues[i];
274
+ float stop2 = stopValues[i + 1];
275
+
276
+ if (value >= stop1 && value <= stop2) {
277
+ float t = (value - stop1) / (stop2 - stop1);
278
+ return mix(colorStops[i], colorStops[i + 1], t);
279
+ }
280
+ }
281
+ return (value <= stopValues[0]) ? colorStops[0] : colorStops[stopCount - 1];
282
+ }
283
+
284
+ void main() {
285
+ float plane_theta = acos(vNormal.z);
286
+ float plane_phi = atan(vNormal.x, vNormal.y);
287
+ plane_phi = mod(plane_phi + 6.28318, 6.28318);
288
+
289
+ vec2 uv = vec2(
290
+ plane_phi / 6.28318, // Normalize phi to [0, 1]
291
+ plane_theta < 1.570795 ? 1.0-(plane_theta / 1.570795) : 0.0 // Normalize theta to [0, 1]
292
+ );
293
+ // Initialize sum
294
+ float totalGHI = 0.0;
295
+
296
+ // Get resolution from texture size
297
+ vec2 resolution = vec2(1.0) / vec2(textureSize(gridsDNI, 0));
298
+
299
+ // Calculate horizon with resolution
300
+ float horizonThetas[${this.horizonResolution}];
301
+ float horizonMountainThetas[${this.horizonResolution}];
302
+ calculateHorizon(horizonThetas, horizonMountainThetas);
303
+
304
+ // Loop through all possible sun positions
305
+ float enlightedArea = 0.0;
306
+ float enlightedAreaWithoutObstacles = 0.0;
307
+ float enlightedAreaWithoutHorizon = 0.0;
308
+ float referenceGHI = 0.0;
309
+ float totalDNIWithoutHorizon = 0.0;
310
+ float totalDNIWithoutObstacles = 0.0;
311
+ float totalDNI = 0.0;
312
+ float totalDHI = 0.0;
313
+ for (float phi = 0.0; phi <= 1.0; phi += resolution.x) {
314
+ for (float theta = 0.0; theta <= 1.0; theta += resolution.y) {
315
+ vec2 sunUV = vec2(phi, theta);
316
+
317
+ // Sample DNI and DHI from textures
318
+ float dni = texture2D(gridsDNI, sunUV).r * maxDNI;
319
+ float dhi = texture2D(gridsDHI, sunUV).r * maxDHI;
320
+
321
+ // Calculate sun direction vector
322
+ float phiRad = phi * 6.28318;
323
+ float thetaRad = theta * 1.57079;
324
+ vec3 sunDir = vec3(
325
+ cos(thetaRad) * sin(phiRad),
326
+ cos(thetaRad) * cos(phiRad),
327
+ sin(thetaRad)
328
+ );
329
+
330
+ // Check if sun is blocked by the horizon
331
+ int phiIndex = getHorizonIndexFromPhiAngle(phiRad);
332
+ bool isBlocked = thetaRad <= horizonThetas[phiIndex];
333
+ bool isBlockedByMountain = thetaRad <= horizonMountainThetas[phiIndex];
334
+
335
+ // Add to total GHI if not blocked
336
+ // Calculate cos_angle using normalVector
337
+ float cos_angle = max(dot(vNormal, sunDir), 0.0);
338
+ referenceGHI += dni * cos_angle + dhi;
339
+ totalDNIWithoutHorizon += dni * cos_angle;
340
+ if(cos_angle > 0.0){
341
+ if (!isBlocked) {
342
+ totalDNI += dni * cos_angle;
343
+ enlightedArea += 2.0 * cos(thetaRad) * sin(thetaRad) * pow((6.28318 * resolution.x) , 2.0);
344
+ }
345
+
346
+ if(!isBlockedByMountain) {
347
+ totalDNIWithoutObstacles += dni * cos_angle;
348
+ enlightedAreaWithoutObstacles += 2.0 * cos(thetaRad) * sin(thetaRad) * pow((6.28318 * resolution.x) , 2.0);
349
+ }
350
+ enlightedAreaWithoutHorizon += 2.0 * cos(thetaRad) * sin(thetaRad) * pow((6.28318 * resolution.x) , 2.0);
351
+ }
352
+
353
+ //compute of Ineichen diffuse
354
+ float F = 1.0 - pow(dhi / max(dni * cos_angle + dhi, 1e-6), 2.0);
355
+ float diffusion_Ineichen = dhi * (1.0 + cos(plane_theta) / 2.0) *
356
+ (1.0 + F * pow(sin(plane_theta / 2.0), 3.0)) *
357
+ (1.0 + F * pow(cos_angle, 2.0) * pow(cos(thetaRad), 3.0));
358
+ totalDHI += diffusion_Ineichen;
359
+ }
360
+ }
361
+ float ground_reflection = GROUND_ALBEDO * referenceGHI * (1.0 - cos(plane_theta)) / 2.0;
362
+ totalGHI = totalDNI + totalDHI * (enlightedArea / enlightedAreaWithoutHorizon) + ground_reflection;
363
+ float totalGHIWithoutHorizon = totalDNIWithoutHorizon + totalDHI + ground_reflection;
364
+
365
+ float totalDHIWithoutObstacles = totalDHI * (enlightedAreaWithoutObstacles / enlightedAreaWithoutHorizon);
366
+ float totalGHIWithoutObstacles = totalDNIWithoutObstacles + totalDHIWithoutObstacles + ground_reflection;
367
+
368
+ // Normalize the total for visualization
369
+ float result = 0.0;
370
+
371
+ if(isShadingFactor == 1) {
372
+ result = 1.0 - (totalGHI / totalGHIWithoutObstacles);
373
+ } else {
374
+ result = totalGHI / (highestGHI);
375
+ }
376
+
377
+ vec3 color;
378
+ if (colorfull == 0) {
379
+ color = vec3(result, result, result);
380
+ } else {
381
+ color = mapValueToColor(result);
382
+ }
383
+ gl_FragColor = vec4(color, 1.0);
384
+ }
385
+ `
386
+ shader.vertexShader = vertexShader
387
+ shader.fragmentShader = fragmentShader
388
+ }
389
+ }
@@ -159,6 +159,16 @@ export default {
159
159
  method: this.renderEdges,
160
160
  arg: [],
161
161
  })
162
+ methodList.push({
163
+ name: 'renderOverlays',
164
+ method: this.renderOverlays,
165
+ arg: [],
166
+ })
167
+ methodList.push({
168
+ name: 'updateProjectionMaterials',
169
+ method: this.updateProjectionMaterials,
170
+ arg: [],
171
+ })
162
172
  case 'marginIsSameChange': //params.polygon
163
173
  case 'marginDifferentChange': //params.polygon,index
164
174
  case 'marginSameChange': //params.polygon
@@ -410,6 +420,13 @@ export default {
410
420
  arg: [],
411
421
  })
412
422
  break
423
+ case 'updateIsYearlyShadingHeatmapActive':
424
+ methodList.push({
425
+ name: 'setModuleFieldsVisibility',
426
+ method: this.setModuleFieldsVisibility,
427
+ arg: [!params.value],
428
+ })
429
+ break
413
430
  case 'selectedPanelChange':
414
431
  methodList.push({
415
432
  name: 'renderModuleFields',
@@ -138,6 +138,8 @@ export async function hydratePolygons({
138
138
  moduleField.data.row_spacing_valley_mm =
139
139
  moduleFieldResp.row_spacing_valley_mm
140
140
  moduleField.data.offset_percent = moduleFieldResp.offset_percent
141
+ moduleField.data.fitting_panel_area_m2 = moduleFieldResp.fitting_panel_area_m2
142
+ moduleField.data.has_fitting_panels = moduleFieldResp.has_fitting_panels
141
143
  moduleField.data.panel_orientation = moduleFieldResp.panel_orientation
142
144
  moduleField.data.panel_direction_degrees =
143
145
  moduleFieldResp.panel_direction_degrees
Binary file
Binary file
@@ -1,43 +0,0 @@
1
- const theme = {
2
- colors: {
3
- primary: '#282387',
4
- secondary: '#818181',
5
- tertiary: '#d5d5d5',
6
- black: '#263238',
7
- yellow: '#fdb813',
8
- darkGray: '#818181',
9
- mediumGray: '#d5d5d5',
10
- lightGray: '#f2f2f2',
11
- white: '#fff',
12
- blue: '#48a2d0',
13
- red: '#FF5656',
14
- blue1: '#e4efff',
15
- blue2: '#F6FAFF',
16
- grey1: '#666',
17
- grey2: '#c4c4c4',
18
- grey3: '#b2b9c5',
19
- grey4: '#dee2eb',
20
- grey5: '#fafafa',
21
- grey6: '#555d61',
22
- turquoise: '#20A4CA',
23
- green: '#99db0c',
24
- purple: '#505ca6',
25
- disabled: '#dfe1e1',
26
- transparentWhite1: '#ffffff32',
27
- transparentBlack1: '#263238e6',
28
- transparentBlue1: '#20a4cae6',
29
- blueElectric: '#66dffa',
30
- eturnityGrey: '#263238'
31
- },
32
-
33
- screen: {
34
- mobileSmall: '345px',
35
- mobile: '425px',
36
- mobileLarge: '530px',
37
- tablet: '768px',
38
- tabletLarge: '950px'
39
- },
40
- borderRadius: '4px'
41
- }
42
-
43
- export default theme
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>