@archvisioninc/canvas 2.8.7 → 3.1.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 +1 -1
- package/dist/helpers/canvasUpdateHelpers.js +178 -5
- package/jsconfig.json +5 -5
- package/package.json +1 -1
- package/public/index.html +21 -21
- package/public/manifest.json +25 -25
- package/public/robots.txt +3 -3
- package/src/helpers/fetchHelpers.js +2 -2
- package/src/package/helpers/canvasUpdateHelpers.js +202 -4
- package/src/scenes/App/App.js +29 -2
- package/src/scenes/App/App.test.js +8 -8
- package/src/setupTests.js +5 -5
package/README.md
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
## GET STARTED
|
|
1
|
+
## GET STARTED
|
|
2
2
|
Please see [developer documentation](https://github.com/ArchvisionInc/archvision-canvas/blob/main/README_DEV.md), in the repository.
|
|
@@ -139,6 +139,115 @@ const applyUVSettings = args => {
|
|
|
139
139
|
texture.vScale = uvYScale;
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
+
|
|
143
|
+
// TODO: Future uses should support red, green, blue, and alpha channels with defaults for each
|
|
144
|
+
const combineMetallicRoughnessTextures = (metallicImage, roughnessImage) => {
|
|
145
|
+
const roughnessHeight = roughnessImage?.naturalHeight ?? 0;
|
|
146
|
+
const roughnessWidth = roughnessImage?.naturalWidth ?? 0;
|
|
147
|
+
const metallicHeight = metallicImage?.naturalHeight ?? 0;
|
|
148
|
+
const metallicWidth = metallicImage?.naturalWidth ?? 0;
|
|
149
|
+
let roughnessData;
|
|
150
|
+
let metallicData;
|
|
151
|
+
const maxHeight = Math.max(...[roughnessHeight, metallicHeight, 1024]);
|
|
152
|
+
const maxWidth = Math.max(...[roughnessWidth, metallicWidth, 1024]);
|
|
153
|
+
const canvas = document.createElement('canvas');
|
|
154
|
+
const ctx = canvas.getContext('2d');
|
|
155
|
+
canvas.width = maxWidth;
|
|
156
|
+
canvas.height = maxHeight;
|
|
157
|
+
if (roughnessImage) {
|
|
158
|
+
ctx.drawImage(roughnessImage, 0, 0, maxWidth, maxHeight);
|
|
159
|
+
roughnessData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
160
|
+
}
|
|
161
|
+
if (metallicImage) {
|
|
162
|
+
ctx.drawImage(metallicImage, 0, 0, maxWidth, maxHeight);
|
|
163
|
+
metallicData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
164
|
+
}
|
|
165
|
+
const combinedImageData = ctx.createImageData(maxWidth, maxHeight);
|
|
166
|
+
const combinedData = combinedImageData.data;
|
|
167
|
+
for (let i = 0; i < combinedData.length; i += 4) {
|
|
168
|
+
combinedData[i] = 255;
|
|
169
|
+
combinedData[i + 1] = roughnessData ? roughnessData[i + 1] : 255;
|
|
170
|
+
combinedData[i + 2] = metallicData ? metallicData[i + 2] : 255;
|
|
171
|
+
combinedData[i + 3] = 255;
|
|
172
|
+
}
|
|
173
|
+
ctx.putImageData(combinedImageData, 0, 0);
|
|
174
|
+
return canvas.toDataURL('image/png');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// eslint-disable-next-line
|
|
178
|
+
const loadImage = async file => {
|
|
179
|
+
return new Promise(resolve => {
|
|
180
|
+
const reader = new FileReader();
|
|
181
|
+
reader.onload = event => {
|
|
182
|
+
const img = new Image();
|
|
183
|
+
img.onload = () => resolve(img);
|
|
184
|
+
img.onerror = () => {
|
|
185
|
+
resolve(null);
|
|
186
|
+
};
|
|
187
|
+
img.src = event.target.result;
|
|
188
|
+
};
|
|
189
|
+
reader.onerror = () => {
|
|
190
|
+
resolve(null);
|
|
191
|
+
};
|
|
192
|
+
reader.readAsDataURL(file);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const textureToImage = texture => {
|
|
196
|
+
return new Promise(resolve => {
|
|
197
|
+
if (!texture.readPixels()) {
|
|
198
|
+
resolve(null);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
texture.readPixels().then(pixels => {
|
|
202
|
+
const canvas = document.createElement('canvas');
|
|
203
|
+
const ctx = canvas.getContext('2d');
|
|
204
|
+
const {
|
|
205
|
+
width,
|
|
206
|
+
height
|
|
207
|
+
} = texture.getSize();
|
|
208
|
+
canvas.width = width;
|
|
209
|
+
canvas.height = height;
|
|
210
|
+
const img = new Image();
|
|
211
|
+
const imageData = ctx.createImageData(width, height);
|
|
212
|
+
imageData.data.set(new Uint8ClampedArray(pixels));
|
|
213
|
+
ctx.putImageData(imageData, 0, 0);
|
|
214
|
+
img.onload = () => {
|
|
215
|
+
resolve(img);
|
|
216
|
+
};
|
|
217
|
+
img.onerror = () => {
|
|
218
|
+
resolve(null);
|
|
219
|
+
};
|
|
220
|
+
img.src = canvas.toDataURL();
|
|
221
|
+
}).catch(() => {
|
|
222
|
+
resolve(null);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
const dataUrlToBlob = dataURI => {
|
|
227
|
+
// convert base64 to raw binary data held in a string
|
|
228
|
+
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
|
229
|
+
const byteString = atob(dataURI.split(',')[1]);
|
|
230
|
+
|
|
231
|
+
// separate out the mime component
|
|
232
|
+
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
233
|
+
|
|
234
|
+
// write the bytes of the string to an ArrayBuffer
|
|
235
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
236
|
+
|
|
237
|
+
// create a view into the buffer
|
|
238
|
+
const ia = new Uint8Array(ab);
|
|
239
|
+
|
|
240
|
+
// set the bytes of the buffer to the correct values
|
|
241
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
242
|
+
ia[i] = byteString.charCodeAt(i);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// write the ArrayBuffer to a blob, and you're done
|
|
246
|
+
const blob = new Blob([ab], {
|
|
247
|
+
type: mimeString
|
|
248
|
+
});
|
|
249
|
+
return blob;
|
|
250
|
+
};
|
|
142
251
|
export const updateMaterial = inboundData => {
|
|
143
252
|
const {
|
|
144
253
|
payload
|
|
@@ -158,6 +267,10 @@ export const updateMaterial = inboundData => {
|
|
|
158
267
|
metallic,
|
|
159
268
|
metallicTexture,
|
|
160
269
|
roughness,
|
|
270
|
+
roughnessTexture,
|
|
271
|
+
ambientColor,
|
|
272
|
+
ambientTextureStrength,
|
|
273
|
+
ambientTexture,
|
|
161
274
|
microSurfaceTexture,
|
|
162
275
|
emissiveColor,
|
|
163
276
|
emissiveIntensity,
|
|
@@ -261,11 +374,31 @@ export const updateMaterial = inboundData => {
|
|
|
261
374
|
// Metallic
|
|
262
375
|
if (metallic !== undefined) material.metallic = metallic;
|
|
263
376
|
if (!_.isEmpty(metallicTexture?.src || metallicTexture?.url)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
material
|
|
377
|
+
// Metallic Texture uses the B channel of the metallicTexture for a PBRMaterial
|
|
378
|
+
const texture = newTexture(metallicTexture?.src || metallicTexture?.url);
|
|
379
|
+
let currentTexture = material.metallicTexture;
|
|
380
|
+
if (!currentTexture) {
|
|
381
|
+
material.metallicTexture = newTexture();
|
|
382
|
+
material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
|
|
383
|
+
currentTexture = material.metallicTexture;
|
|
384
|
+
}
|
|
385
|
+
const metallicBlob = dataUrlToBlob(texture.url);
|
|
386
|
+
Promise.all([loadImage(metallicBlob), textureToImage(currentTexture)]).then(data => {
|
|
387
|
+
const [metallicImage, currentImage] = data;
|
|
388
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, currentImage);
|
|
389
|
+
currentTexture.updateURL(combinedTexture);
|
|
390
|
+
texture.dispose();
|
|
391
|
+
applyUVSettings({
|
|
392
|
+
texture: material.metallicTexture,
|
|
393
|
+
material
|
|
394
|
+
});
|
|
395
|
+
newMetaDataEntry('materials', buildMaterialsArray());
|
|
396
|
+
newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
|
|
397
|
+
}).catch(err => {
|
|
398
|
+
console.log(`Error updating the roughness texture of material: ${material.name}`);
|
|
399
|
+
console.log({
|
|
400
|
+
err
|
|
401
|
+
});
|
|
269
402
|
});
|
|
270
403
|
}
|
|
271
404
|
|
|
@@ -279,6 +412,46 @@ export const updateMaterial = inboundData => {
|
|
|
279
412
|
material
|
|
280
413
|
});
|
|
281
414
|
}
|
|
415
|
+
if (!_.isEmpty(roughnessTexture?.src || roughnessTexture?.url)) {
|
|
416
|
+
// Roughness Texture uses the G channel of the metallicTexture for a PBRMaterial
|
|
417
|
+
const texture = newTexture(roughnessTexture?.src || roughnessTexture?.url);
|
|
418
|
+
let currentTexture = material.metallicTexture;
|
|
419
|
+
if (!currentTexture) {
|
|
420
|
+
material.metallicTexture = newTexture();
|
|
421
|
+
material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
|
|
422
|
+
currentTexture = material.metallicTexture;
|
|
423
|
+
}
|
|
424
|
+
const roughnessBlob = dataUrlToBlob(texture.url);
|
|
425
|
+
Promise.all([loadImage(roughnessBlob), textureToImage(currentTexture)]).then(data => {
|
|
426
|
+
const [roughnessImage, metallicImage] = data;
|
|
427
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, roughnessImage);
|
|
428
|
+
currentTexture.updateURL(combinedTexture);
|
|
429
|
+
texture.dispose();
|
|
430
|
+
applyUVSettings({
|
|
431
|
+
texture: material.metallicTexture,
|
|
432
|
+
material
|
|
433
|
+
});
|
|
434
|
+
newMetaDataEntry('materials', buildMaterialsArray());
|
|
435
|
+
newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
|
|
436
|
+
}).catch(err => {
|
|
437
|
+
console.log(`Error updating the roughness texture of material: ${material.name}`);
|
|
438
|
+
console.log({
|
|
439
|
+
err
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Ambient
|
|
445
|
+
if (ambientColor) material.ambientColor = newColor(emissiveColor);
|
|
446
|
+
if (ambientTextureStrength !== undefined) material.ambientTextureStrength = ambientTextureStrength;
|
|
447
|
+
if (!_.isEmpty(ambientTexture?.src || ambientTexture?.url)) {
|
|
448
|
+
material.ambientTexture = newTexture(ambientTexture?.src || ambientTexture?.url);
|
|
449
|
+
material.ambientTexture.name = ambientTexture.name;
|
|
450
|
+
applyUVSettings({
|
|
451
|
+
texture: material.ambientTexture,
|
|
452
|
+
material
|
|
453
|
+
});
|
|
454
|
+
}
|
|
282
455
|
|
|
283
456
|
// Emissive
|
|
284
457
|
if (emissiveColor) material.emissiveColor = newColor(emissiveColor);
|
package/jsconfig.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"baseUrl": "src"
|
|
4
|
-
},
|
|
5
|
-
"include": ["src"]
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": "src"
|
|
4
|
+
},
|
|
5
|
+
"include": ["src"]
|
|
6
6
|
}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
-
<meta name="theme-color" content="#000000" />
|
|
8
|
-
<meta
|
|
9
|
-
name="description"
|
|
10
|
-
content="Web site created using create-react-app"
|
|
11
|
-
/>
|
|
12
|
-
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
13
|
-
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
14
|
-
<title>React App</title>
|
|
15
|
-
</head>
|
|
16
|
-
|
|
17
|
-
<body>
|
|
18
|
-
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
19
|
-
<div id="root"></div>
|
|
20
|
-
</body>
|
|
21
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
<meta
|
|
9
|
+
name="description"
|
|
10
|
+
content="Web site created using create-react-app"
|
|
11
|
+
/>
|
|
12
|
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
13
|
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
14
|
+
<title>React App</title>
|
|
15
|
+
</head>
|
|
16
|
+
|
|
17
|
+
<body>
|
|
18
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
19
|
+
<div id="root"></div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
package/public/manifest.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"short_name": "React App",
|
|
3
|
-
"name": "Create React App Sample",
|
|
4
|
-
"icons": [
|
|
5
|
-
{
|
|
6
|
-
"src": "favicon.ico",
|
|
7
|
-
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
-
"type": "image/x-icon"
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"src": "logo192.png",
|
|
12
|
-
"type": "image/png",
|
|
13
|
-
"sizes": "192x192"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"src": "logo512.png",
|
|
17
|
-
"type": "image/png",
|
|
18
|
-
"sizes": "512x512"
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"start_url": ".",
|
|
22
|
-
"display": "standalone",
|
|
23
|
-
"theme_color": "#000000",
|
|
24
|
-
"background_color": "#ffffff"
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"short_name": "React App",
|
|
3
|
+
"name": "Create React App Sample",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
package/public/robots.txt
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# https://www.robotstxt.org/robotstxt.html
|
|
2
|
-
User-agent: *
|
|
3
|
-
Disallow:
|
|
1
|
+
# https://www.robotstxt.org/robotstxt.html
|
|
2
|
+
User-agent: *
|
|
3
|
+
Disallow:
|
|
@@ -14,7 +14,7 @@ export const fetchDownloadURL = async args => {
|
|
|
14
14
|
if (!_.isEmpty(rpcGuid)) {
|
|
15
15
|
const servicesURL = `https://api.archvision.services/rpc/v1/integration/${rpcGuid}/download/${format}`;
|
|
16
16
|
// eslint-disable-next-line
|
|
17
|
-
const token = '
|
|
17
|
+
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjE2MDAyMDc3ODQifQ.eyJpZCI6ImowbzVobDJkdXc5ZXZ4bXJubGIxZWh6a3lhYmJmN3d6ZzN3Z2g3c2kiLCJqdGkiOiJqMG81aGwyZHV3OWV2eG1ybmxiMWVoemt5YWJiZjd3emczd2doN3NpIiwiaXNzIjoiaHR0cHM6XC9cL2FyY2h2aXNpb24uY29tIiwiYXVkIjoiNDVFUnF3SEV5SWRqbXhkaUU4MUxMbDlFYTN2SzZZQ3dLNWE2R2xEMyIsInN1YiI6IjM5MDQxOSIsImV4cCI6MTc0ODI3ODEwNiwiaWF0IjoxNzE3MTc4MTA2LCJ0b2tlbl90eXBlIjoiYmVhcmVyIiwic2NvcGUiOiJiYXNpYyJ9.xKpX0xCEdoVpn-GGdmRWcMPp3pEa7PUbMxlhuAsNjL1ZoFIbxwTrAoXDnvozxjdxomxM7nFw05I1caGtfJARPipOSLFgzD1ANYvogmAImg1zXUV8RsHEVaApxhdwUS7IPO13K3QJMGmn-NgqpYly9ZF9XYxZ7twT4JneGmc_WzeairT9qqGLhKXuU_tkJ49tKAXfCSHAFFBkb93nnsrpd-RlsIeQnc7puTqxlKTREzvmW3_RdqO_1X9ohlnn3wWBEFo_a0W770lfWoF1JlJIqd9NIPrgVRG5inRmYNEz6gw5yLHcDDG-Z5DrQExVUJkYV81wBK2krr0AgX45CWTEdQ';
|
|
18
18
|
const options = {
|
|
19
19
|
method: 'GET',
|
|
20
20
|
headers: {
|
|
@@ -42,4 +42,4 @@ export const fetchDownloadURL = async args => {
|
|
|
42
42
|
clearExisting,
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
-
};
|
|
45
|
+
};
|
|
@@ -177,6 +177,134 @@ const applyUVSettings = args => {
|
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
+
// TODO: Future uses should support red, green, blue, and alpha channels with defaults for each
|
|
181
|
+
const combineMetallicRoughnessTextures = (metallicImage, roughnessImage) => {
|
|
182
|
+
const roughnessHeight = roughnessImage?.naturalHeight ?? 0;
|
|
183
|
+
const roughnessWidth = roughnessImage?.naturalWidth ?? 0;
|
|
184
|
+
const metallicHeight = metallicImage?.naturalHeight ?? 0;
|
|
185
|
+
const metallicWidth = metallicImage?.naturalWidth ?? 0;
|
|
186
|
+
|
|
187
|
+
let roughnessData;
|
|
188
|
+
let metallicData;
|
|
189
|
+
|
|
190
|
+
const maxHeight = Math.max(...[ roughnessHeight, metallicHeight, 1024 ]);
|
|
191
|
+
const maxWidth = Math.max(...[ roughnessWidth, metallicWidth, 1024 ]);
|
|
192
|
+
|
|
193
|
+
const canvas = document.createElement('canvas');
|
|
194
|
+
|
|
195
|
+
const ctx = canvas.getContext('2d');
|
|
196
|
+
|
|
197
|
+
canvas.width = maxWidth;
|
|
198
|
+
canvas.height = maxHeight;
|
|
199
|
+
|
|
200
|
+
if (roughnessImage) {
|
|
201
|
+
ctx.drawImage(roughnessImage, 0, 0, maxWidth, maxHeight);
|
|
202
|
+
roughnessData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (metallicImage) {
|
|
206
|
+
ctx.drawImage(metallicImage, 0, 0, maxWidth, maxHeight);
|
|
207
|
+
metallicData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const combinedImageData = ctx.createImageData(maxWidth, maxHeight);
|
|
211
|
+
const combinedData = combinedImageData.data;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < combinedData.length; i += 4) {
|
|
214
|
+
combinedData[i] = 255;
|
|
215
|
+
combinedData[i + 1] = roughnessData ? roughnessData[i + 1] : 255;
|
|
216
|
+
combinedData[i + 2] = metallicData ? metallicData[i + 2] : 255;
|
|
217
|
+
combinedData[i + 3] = 255;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ctx.putImageData(combinedImageData, 0, 0);
|
|
221
|
+
|
|
222
|
+
return canvas.toDataURL('image/png');
|
|
223
|
+
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// eslint-disable-next-line
|
|
227
|
+
const loadImage = async file => {
|
|
228
|
+
return new Promise(resolve => {
|
|
229
|
+
const reader = new FileReader();
|
|
230
|
+
reader.onload = event => {
|
|
231
|
+
const img = new Image();
|
|
232
|
+
img.onload = () => resolve(img);
|
|
233
|
+
img.onerror = () => {
|
|
234
|
+
resolve(null);
|
|
235
|
+
};
|
|
236
|
+
img.src = event.target.result;
|
|
237
|
+
};
|
|
238
|
+
reader.onerror = () => {
|
|
239
|
+
resolve(null);
|
|
240
|
+
};
|
|
241
|
+
reader.readAsDataURL(file);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const textureToImage = texture => {
|
|
246
|
+
return new Promise(resolve => {
|
|
247
|
+
if (!texture.readPixels()) {
|
|
248
|
+
resolve(null);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
texture.readPixels()
|
|
252
|
+
.then(pixels => {
|
|
253
|
+
const canvas = document.createElement('canvas');
|
|
254
|
+
const ctx = canvas.getContext('2d');
|
|
255
|
+
const { width, height } = texture.getSize();
|
|
256
|
+
|
|
257
|
+
canvas.width = width;
|
|
258
|
+
canvas.height = height;
|
|
259
|
+
|
|
260
|
+
const img = new Image();
|
|
261
|
+
const imageData = ctx.createImageData(width, height);
|
|
262
|
+
imageData.data.set(new Uint8ClampedArray(pixels));
|
|
263
|
+
|
|
264
|
+
ctx.putImageData(imageData, 0, 0);
|
|
265
|
+
|
|
266
|
+
img.onload = () => {
|
|
267
|
+
resolve(img);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
img.onerror = () => {
|
|
271
|
+
resolve(null);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
img.src = canvas.toDataURL();
|
|
276
|
+
})
|
|
277
|
+
.catch(() => {
|
|
278
|
+
resolve(null);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const dataUrlToBlob = dataURI => {
|
|
285
|
+
// convert base64 to raw binary data held in a string
|
|
286
|
+
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
|
287
|
+
const byteString = atob(dataURI.split(',')[1]);
|
|
288
|
+
|
|
289
|
+
// separate out the mime component
|
|
290
|
+
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
291
|
+
|
|
292
|
+
// write the bytes of the string to an ArrayBuffer
|
|
293
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
294
|
+
|
|
295
|
+
// create a view into the buffer
|
|
296
|
+
const ia = new Uint8Array(ab);
|
|
297
|
+
|
|
298
|
+
// set the bytes of the buffer to the correct values
|
|
299
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
300
|
+
ia[i] = byteString.charCodeAt(i);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// write the ArrayBuffer to a blob, and you're done
|
|
304
|
+
const blob = new Blob([ ab ], { type: mimeString });
|
|
305
|
+
return blob;
|
|
306
|
+
};
|
|
307
|
+
|
|
180
308
|
export const updateMaterial = inboundData => {
|
|
181
309
|
const { payload } = inboundData;
|
|
182
310
|
const {
|
|
@@ -194,6 +322,10 @@ export const updateMaterial = inboundData => {
|
|
|
194
322
|
metallic,
|
|
195
323
|
metallicTexture,
|
|
196
324
|
roughness,
|
|
325
|
+
roughnessTexture,
|
|
326
|
+
ambientColor,
|
|
327
|
+
ambientTextureStrength,
|
|
328
|
+
ambientTexture,
|
|
197
329
|
microSurfaceTexture,
|
|
198
330
|
emissiveColor,
|
|
199
331
|
emissiveIntensity,
|
|
@@ -293,10 +425,35 @@ export const updateMaterial = inboundData => {
|
|
|
293
425
|
// Metallic
|
|
294
426
|
if (metallic !== undefined) material.metallic = metallic;
|
|
295
427
|
if (!_.isEmpty(metallicTexture?.src || metallicTexture?.url)) {
|
|
296
|
-
|
|
297
|
-
|
|
428
|
+
// Metallic Texture uses the B channel of the metallicTexture for a PBRMaterial
|
|
429
|
+
const texture = newTexture(metallicTexture?.src || metallicTexture?.url);
|
|
430
|
+
let currentTexture = material.metallicTexture;
|
|
431
|
+
if (!currentTexture) {
|
|
432
|
+
material.metallicTexture = newTexture();
|
|
433
|
+
material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
|
|
434
|
+
currentTexture = material.metallicTexture;
|
|
435
|
+
}
|
|
298
436
|
|
|
299
|
-
|
|
437
|
+
const metallicBlob = dataUrlToBlob(texture.url);
|
|
438
|
+
|
|
439
|
+
Promise.all([
|
|
440
|
+
loadImage(metallicBlob),
|
|
441
|
+
textureToImage(currentTexture),
|
|
442
|
+
])
|
|
443
|
+
.then(data => {
|
|
444
|
+
const [ metallicImage, currentImage ] = data;
|
|
445
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, currentImage);
|
|
446
|
+
currentTexture.updateURL(combinedTexture);
|
|
447
|
+
|
|
448
|
+
texture.dispose();
|
|
449
|
+
applyUVSettings({ texture: material.metallicTexture, material });
|
|
450
|
+
newMetaDataEntry('materials', buildMaterialsArray());
|
|
451
|
+
newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
|
|
452
|
+
})
|
|
453
|
+
.catch(err => {
|
|
454
|
+
console.log(`Error updating the roughness texture of material: ${material.name}`);
|
|
455
|
+
console.log({ err });
|
|
456
|
+
});
|
|
300
457
|
}
|
|
301
458
|
|
|
302
459
|
// Roughness
|
|
@@ -308,6 +465,47 @@ export const updateMaterial = inboundData => {
|
|
|
308
465
|
applyUVSettings({ texture: material.microSurfaceTexture, material });
|
|
309
466
|
}
|
|
310
467
|
|
|
468
|
+
if (!_.isEmpty(roughnessTexture?.src || roughnessTexture?.url)) {
|
|
469
|
+
// Roughness Texture uses the G channel of the metallicTexture for a PBRMaterial
|
|
470
|
+
const texture = newTexture(roughnessTexture?.src || roughnessTexture?.url);
|
|
471
|
+
let currentTexture = material.metallicTexture;
|
|
472
|
+
if (!currentTexture) {
|
|
473
|
+
material.metallicTexture = newTexture();
|
|
474
|
+
material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
|
|
475
|
+
currentTexture = material.metallicTexture;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const roughnessBlob = dataUrlToBlob(texture.url);
|
|
479
|
+
|
|
480
|
+
Promise.all([ loadImage(roughnessBlob), textureToImage(currentTexture) ])
|
|
481
|
+
.then(data => {
|
|
482
|
+
const [ roughnessImage, metallicImage ] = data;
|
|
483
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, roughnessImage);
|
|
484
|
+
currentTexture.updateURL(combinedTexture);
|
|
485
|
+
|
|
486
|
+
texture.dispose();
|
|
487
|
+
applyUVSettings({ texture: material.metallicTexture, material });
|
|
488
|
+
newMetaDataEntry('materials', buildMaterialsArray());
|
|
489
|
+
newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
|
|
490
|
+
})
|
|
491
|
+
.catch(err => {
|
|
492
|
+
console.log(`Error updating the roughness texture of material: ${material.name}`);
|
|
493
|
+
console.log({ err });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Ambient
|
|
499
|
+
if (ambientColor) material.ambientColor = newColor(emissiveColor);
|
|
500
|
+
if (ambientTextureStrength !== undefined) material.ambientTextureStrength = ambientTextureStrength;
|
|
501
|
+
if (!_.isEmpty(ambientTexture?.src || ambientTexture?.url)) {
|
|
502
|
+
material.ambientTexture = newTexture(ambientTexture?.src || ambientTexture?.url);
|
|
503
|
+
material.ambientTexture.name = ambientTexture.name;
|
|
504
|
+
|
|
505
|
+
applyUVSettings({ texture: material.ambientTexture, material });
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
311
509
|
// Emissive
|
|
312
510
|
if (emissiveColor) material.emissiveColor = newColor(emissiveColor);
|
|
313
511
|
if (emissiveIntensity !== undefined) material.emissiveIntensity = emissiveIntensity;
|
|
@@ -948,4 +1146,4 @@ export const updatePublish = inboundData => {
|
|
|
948
1146
|
};
|
|
949
1147
|
|
|
950
1148
|
newMetaDataEntry('publish', withOptimizationValues);
|
|
951
|
-
};
|
|
1149
|
+
};
|
package/src/scenes/App/App.js
CHANGED
|
@@ -9,7 +9,7 @@ import { updateMaterial, scene } from 'package/helpers';
|
|
|
9
9
|
import Canvas from 'package/Canvas';
|
|
10
10
|
import _ from 'lodash';
|
|
11
11
|
|
|
12
|
-
const materialMode =
|
|
12
|
+
const materialMode = false;
|
|
13
13
|
const previewMode = false;
|
|
14
14
|
const demoScene = false;
|
|
15
15
|
const shaderballGuid = '21-791A-0D8D-F8A0-63F7-5086-471F-69A7-7AB0-00';
|
|
@@ -127,6 +127,27 @@ const App = () => {
|
|
|
127
127
|
const updatedArray = notifications.filter((item, i) => index !== i);
|
|
128
128
|
setNotifications(updatedArray);
|
|
129
129
|
};
|
|
130
|
+
const onImageLoad = args => {
|
|
131
|
+
const inboundData = {
|
|
132
|
+
payload: {
|
|
133
|
+
id: 'Material Metal PBR Rich',
|
|
134
|
+
metallicTexture: args,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
updateMaterial(inboundData);
|
|
139
|
+
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleUpload = e => {
|
|
143
|
+
const file = e.currentTarget.files[0];
|
|
144
|
+
const reader = new FileReader();
|
|
145
|
+
reader.readAsDataURL(file);
|
|
146
|
+
reader.onload = () => onImageLoad({
|
|
147
|
+
name: file.name,
|
|
148
|
+
src: reader.result,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
130
151
|
|
|
131
152
|
useEffect(() => {
|
|
132
153
|
getPreviewURL();
|
|
@@ -189,6 +210,12 @@ const App = () => {
|
|
|
189
210
|
Import material
|
|
190
211
|
</Button>
|
|
191
212
|
|
|
213
|
+
<input
|
|
214
|
+
type={'file'}
|
|
215
|
+
onChange={handleUpload}
|
|
216
|
+
accept={'image/*'}
|
|
217
|
+
/>
|
|
218
|
+
|
|
192
219
|
<Button
|
|
193
220
|
theme={theme}
|
|
194
221
|
$selectedTheme={selectedTheme}
|
|
@@ -249,4 +276,4 @@ const App = () => {
|
|
|
249
276
|
);
|
|
250
277
|
};
|
|
251
278
|
|
|
252
|
-
export default App;
|
|
279
|
+
export default App;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import App from './App';
|
|
3
|
-
|
|
4
|
-
test('renders learn react link', () => {
|
|
5
|
-
render(<App />);
|
|
6
|
-
const linkElement = screen.getByText(/learn react/i);
|
|
7
|
-
expect(linkElement).toBeInTheDocument();
|
|
8
|
-
});
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import App from './App';
|
|
3
|
+
|
|
4
|
+
test('renders learn react link', () => {
|
|
5
|
+
render(<App />);
|
|
6
|
+
const linkElement = screen.getByText(/learn react/i);
|
|
7
|
+
expect(linkElement).toBeInTheDocument();
|
|
8
|
+
});
|
package/src/setupTests.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
2
|
-
// allows you to do things like:
|
|
3
|
-
// expect(element).toHaveTextContent(/react/i)
|
|
4
|
-
// learn more: https://github.com/testing-library/jest-dom
|
|
5
|
-
import '@testing-library/jest-dom';
|
|
1
|
+
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
2
|
+
// allows you to do things like:
|
|
3
|
+
// expect(element).toHaveTextContent(/react/i)
|
|
4
|
+
// learn more: https://github.com/testing-library/jest-dom
|
|
5
|
+
import '@testing-library/jest-dom';
|