@archvisioninc/canvas 2.8.6 → 3.0.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/README_DEV.md +6 -0
- package/dist/helpers/canvasUpdateHelpers.js +135 -6
- package/dist/helpers/utilityHelpers.js +24 -22
- 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 +158 -4
- package/src/package/helpers/utilityHelpers.js +29 -24
- package/src/scenes/App/App.js +22 -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.
|
package/README_DEV.md
CHANGED
|
@@ -152,6 +152,12 @@ a UI action/change from one of FOVEA's panels.
|
|
|
152
152
|
* **setSerializedData**: Function type -- Canvas calls this any time something in Babylon has changed (usually from the `updateData` payload coming in to Canvas),
|
|
153
153
|
and the host app needs that updated data to re-render it's UI.
|
|
154
154
|
* **setBillboardImages**: Function type -- niche function only used when `updateData` payload comes in requesting screenshots for billboard proxy types.
|
|
155
|
+
* **integration**: Object type -- allows you to set specific overrides to the viewport. Currently only two options are supported:
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
// NOTE: Both of these should be boolean values
|
|
159
|
+
{ shadows, meshHightling }
|
|
160
|
+
```
|
|
155
161
|
|
|
156
162
|
### Preview Canvas Mode
|
|
157
163
|
The preview mode is a simplified version of the standard canvas that hides the grid and other viewport widgets, limiting certain shortcuts and event listeners etc.
|
|
@@ -139,6 +139,102 @@ 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;
|
|
146
|
+
const roughnessWidth = roughnessImage.naturalWidth;
|
|
147
|
+
const metallicHeight = metallicImage.naturalHeight;
|
|
148
|
+
const metallicWidth = metallicImage.naturalWidth;
|
|
149
|
+
const maxHeight = Math.max(roughnessHeight, metallicHeight);
|
|
150
|
+
const maxWidth = Math.max(roughnessWidth, metallicWidth);
|
|
151
|
+
const canvas = document.createElement('canvas');
|
|
152
|
+
const ctx = canvas.getContext('2d');
|
|
153
|
+
canvas.width = maxWidth;
|
|
154
|
+
canvas.height = maxHeight;
|
|
155
|
+
ctx.drawImage(roughnessImage, 0, 0, maxWidth, maxHeight);
|
|
156
|
+
const roughnessData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
157
|
+
ctx.drawImage(metallicImage, 0, 0, maxWidth, maxHeight);
|
|
158
|
+
const metallicData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
159
|
+
const combinedImageData = ctx.createImageData(maxWidth, maxHeight);
|
|
160
|
+
const combinedData = combinedImageData.data;
|
|
161
|
+
for (let i = 0; i < combinedData.length; i += 4) {
|
|
162
|
+
combinedData[i] = 255;
|
|
163
|
+
combinedData[i + 1] = roughnessData[i + 1];
|
|
164
|
+
combinedData[i + 2] = metallicData[i + 2];
|
|
165
|
+
combinedData[i + 3] = 255;
|
|
166
|
+
}
|
|
167
|
+
ctx.putImageData(combinedImageData, 0, 0);
|
|
168
|
+
return canvas.toDataURL('image/png');
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// eslint-disable-next-line
|
|
172
|
+
const loadImage = async file => {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const reader = new FileReader();
|
|
175
|
+
reader.onload = event => {
|
|
176
|
+
const img = new Image();
|
|
177
|
+
img.onload = () => {
|
|
178
|
+
resolve(img);
|
|
179
|
+
};
|
|
180
|
+
img.onerror = reject;
|
|
181
|
+
img.src = event.target.result;
|
|
182
|
+
};
|
|
183
|
+
reader.onerror = reject;
|
|
184
|
+
reader.readAsDataURL(file);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const textureToImage = (texture, textureName) => {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
texture.readPixels().then(pixels => {
|
|
190
|
+
const canvas = document.createElement('canvas');
|
|
191
|
+
const ctx = canvas.getContext('2d');
|
|
192
|
+
const {
|
|
193
|
+
width,
|
|
194
|
+
height
|
|
195
|
+
} = texture.getSize();
|
|
196
|
+
canvas.width = width;
|
|
197
|
+
canvas.height = height;
|
|
198
|
+
const img = new Image();
|
|
199
|
+
const imageData = ctx.createImageData(width, height);
|
|
200
|
+
imageData.data.set(new Uint8ClampedArray(pixels));
|
|
201
|
+
ctx.putImageData(imageData, 0, 0);
|
|
202
|
+
img.onload = () => {
|
|
203
|
+
resolve(img);
|
|
204
|
+
};
|
|
205
|
+
img.onerror = event => {
|
|
206
|
+
console.log(`Error loading image: ${textureName}`);
|
|
207
|
+
reject(event);
|
|
208
|
+
};
|
|
209
|
+
img.src = canvas.toDataURL();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const dataUrlToBlob = dataURI => {
|
|
214
|
+
// convert base64 to raw binary data held in a string
|
|
215
|
+
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
|
216
|
+
const byteString = atob(dataURI.split(',')[1]);
|
|
217
|
+
|
|
218
|
+
// separate out the mime component
|
|
219
|
+
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
220
|
+
|
|
221
|
+
// write the bytes of the string to an ArrayBuffer
|
|
222
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
223
|
+
|
|
224
|
+
// create a view into the buffer
|
|
225
|
+
const ia = new Uint8Array(ab);
|
|
226
|
+
|
|
227
|
+
// set the bytes of the buffer to the correct values
|
|
228
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
229
|
+
ia[i] = byteString.charCodeAt(i);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// write the ArrayBuffer to a blob, and you're done
|
|
233
|
+
const blob = new Blob([ab], {
|
|
234
|
+
type: mimeString
|
|
235
|
+
});
|
|
236
|
+
return blob;
|
|
237
|
+
};
|
|
142
238
|
export const updateMaterial = inboundData => {
|
|
143
239
|
const {
|
|
144
240
|
payload
|
|
@@ -158,6 +254,7 @@ export const updateMaterial = inboundData => {
|
|
|
158
254
|
metallic,
|
|
159
255
|
metallicTexture,
|
|
160
256
|
roughness,
|
|
257
|
+
roughnessTexture,
|
|
161
258
|
microSurfaceTexture,
|
|
162
259
|
emissiveColor,
|
|
163
260
|
emissiveIntensity,
|
|
@@ -261,12 +358,24 @@ export const updateMaterial = inboundData => {
|
|
|
261
358
|
// Metallic
|
|
262
359
|
if (metallic !== undefined) material.metallic = metallic;
|
|
263
360
|
if (!_.isEmpty(metallicTexture?.src || metallicTexture?.url)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
material
|
|
269
|
-
|
|
361
|
+
// Metallic Texture uses the B channel of the metallicTexture for a PBRMaterial
|
|
362
|
+
const texture = newTexture(metallicTexture?.src || metallicTexture?.url);
|
|
363
|
+
let currentTexture = material.metallicTexture;
|
|
364
|
+
if (!currentTexture) {
|
|
365
|
+
material.metallicTexture = newTexture();
|
|
366
|
+
currentTexture = material.metallicTexture;
|
|
367
|
+
}
|
|
368
|
+
const metallicBlob = dataUrlToBlob(texture.url);
|
|
369
|
+
Promise.all([loadImage(metallicBlob), textureToImage(currentTexture, 'metallic')]).then(data => {
|
|
370
|
+
const [metallicImage, currentImage] = data;
|
|
371
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, currentImage);
|
|
372
|
+
currentTexture.updateURL(combinedTexture);
|
|
373
|
+
texture.dispose();
|
|
374
|
+
applyUVSettings({
|
|
375
|
+
texture: material.metallicTexture,
|
|
376
|
+
material
|
|
377
|
+
});
|
|
378
|
+
}).catch(() => console.log(`Error updating the metallic texture of material: ${material.name}`));
|
|
270
379
|
}
|
|
271
380
|
|
|
272
381
|
// Roughness
|
|
@@ -279,6 +388,26 @@ export const updateMaterial = inboundData => {
|
|
|
279
388
|
material
|
|
280
389
|
});
|
|
281
390
|
}
|
|
391
|
+
if (!_.isEmpty(roughnessTexture?.src || roughnessTexture?.url)) {
|
|
392
|
+
// Roughness Texture uses the G channel of the metallicTexture for a PBRMaterial
|
|
393
|
+
const texture = newTexture(roughnessTexture?.src || roughnessTexture?.url);
|
|
394
|
+
let currentTexture = material.metallicTexture;
|
|
395
|
+
if (!currentTexture) {
|
|
396
|
+
material.metallicTexture = newTexture();
|
|
397
|
+
currentTexture = material.metallicTexture;
|
|
398
|
+
}
|
|
399
|
+
const roughnessBlob = dataUrlToBlob(texture.url);
|
|
400
|
+
Promise.all([loadImage(roughnessBlob), textureToImage(currentTexture, 'metallic')]).then(data => {
|
|
401
|
+
const [roughnessImage, metallicImage] = data;
|
|
402
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, roughnessImage);
|
|
403
|
+
currentTexture.updateURL(combinedTexture);
|
|
404
|
+
texture.dispose();
|
|
405
|
+
applyUVSettings({
|
|
406
|
+
texture: material.metallicTexture,
|
|
407
|
+
material
|
|
408
|
+
});
|
|
409
|
+
}).catch(() => console.log(`Error updating the roughness texture of material: ${material.name}`));
|
|
410
|
+
}
|
|
282
411
|
|
|
283
412
|
// Emissive
|
|
284
413
|
if (emissiveColor) material.emissiveColor = newColor(emissiveColor);
|
|
@@ -275,7 +275,7 @@ export const buildSelectedMaterialArray = () => {
|
|
|
275
275
|
const mainMaterial = scene.metadata?.materials?.find?.(item => item.materialId === 'material');
|
|
276
276
|
return [mainMaterial];
|
|
277
277
|
}
|
|
278
|
-
const arr = selectedMeshes
|
|
278
|
+
const arr = selectedMeshes?.map?.(mesh => {
|
|
279
279
|
const material = mesh.material;
|
|
280
280
|
if (material) {
|
|
281
281
|
const existingMaterial = scene.metadata.materials?.find(mat => mat.materialId === material.id);
|
|
@@ -464,28 +464,30 @@ export const getExistingUVSettings = args => {
|
|
|
464
464
|
};
|
|
465
465
|
const userMaterials = getUserMaterials();
|
|
466
466
|
const material = userMaterials.find(mat => mat.id === materialId);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
467
|
+
if (material) {
|
|
468
|
+
Object.keys(material)?.find?.(key => {
|
|
469
|
+
const validMaterialWithTexture = _.isObject(material[key]) && !_.isArray(material[key]) && key.endsWith('Texture');
|
|
470
|
+
const texture = material[key];
|
|
471
|
+
const isEnvironment = key.toLowerCase().includes('environment');
|
|
472
|
+
const hasAllKeys = texture && ['uAng', 'wAng', 'vAng', 'uOffset', 'vOffset', 'uScale', 'vScale'].every(key => texture[key] !== undefined);
|
|
472
473
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
474
|
+
// NOTE: All UV's are adjusted globally, at the same time in canvasUpdateHelpers.js
|
|
475
|
+
// when **any** UV setting is changed. So we only need to find the first one to
|
|
476
|
+
// restore the UV settings.
|
|
477
|
+
if (!isEnvironment && validMaterialWithTexture && hasAllKeys && !settingsFound) {
|
|
478
|
+
settingsFound = true;
|
|
479
|
+
uvSettings = {
|
|
480
|
+
uvXScale: texture.uScale,
|
|
481
|
+
uvYScale: texture.vScale,
|
|
482
|
+
uvXRotation: texture.uAng,
|
|
483
|
+
uvYRotation: texture.wAng,
|
|
484
|
+
uvZRotation: texture.vAng,
|
|
485
|
+
uvXOffset: texture.uOffset,
|
|
486
|
+
uvYOffset: texture.vOffset
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
489
491
|
return uvSettings;
|
|
490
492
|
};
|
|
491
493
|
export const materialData = material => {
|
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,119 @@ 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;
|
|
183
|
+
const roughnessWidth = roughnessImage.naturalWidth;
|
|
184
|
+
const metallicHeight = metallicImage.naturalHeight;
|
|
185
|
+
const metallicWidth = metallicImage.naturalWidth;
|
|
186
|
+
|
|
187
|
+
const maxHeight = Math.max(roughnessHeight, metallicHeight);
|
|
188
|
+
const maxWidth = Math.max(roughnessWidth, metallicWidth);
|
|
189
|
+
|
|
190
|
+
const canvas = document.createElement('canvas');
|
|
191
|
+
|
|
192
|
+
const ctx = canvas.getContext('2d');
|
|
193
|
+
|
|
194
|
+
canvas.width = maxWidth;
|
|
195
|
+
canvas.height = maxHeight;
|
|
196
|
+
|
|
197
|
+
ctx.drawImage(roughnessImage, 0, 0, maxWidth, maxHeight);
|
|
198
|
+
const roughnessData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
199
|
+
|
|
200
|
+
ctx.drawImage(metallicImage, 0, 0, maxWidth, maxHeight);
|
|
201
|
+
const metallicData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
|
|
202
|
+
|
|
203
|
+
const combinedImageData = ctx.createImageData(maxWidth, maxHeight);
|
|
204
|
+
const combinedData = combinedImageData.data;
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < combinedData.length; i += 4) {
|
|
207
|
+
combinedData[i] = 255;
|
|
208
|
+
combinedData[i + 1] = roughnessData[i + 1];
|
|
209
|
+
combinedData[i + 2] = metallicData[i + 2];
|
|
210
|
+
combinedData[i + 3] = 255;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
ctx.putImageData(combinedImageData, 0, 0);
|
|
214
|
+
|
|
215
|
+
return canvas.toDataURL('image/png');
|
|
216
|
+
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// eslint-disable-next-line
|
|
220
|
+
const loadImage = async file => {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const reader = new FileReader();
|
|
223
|
+
reader.onload = event => {
|
|
224
|
+
const img = new Image();
|
|
225
|
+
img.onload = () => {
|
|
226
|
+
resolve(img);
|
|
227
|
+
};
|
|
228
|
+
img.onerror = reject;
|
|
229
|
+
img.src = event.target.result;
|
|
230
|
+
};
|
|
231
|
+
reader.onerror = reject;
|
|
232
|
+
reader.readAsDataURL(file);
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const textureToImage = (texture, textureName) => {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
texture.readPixels()
|
|
239
|
+
.then(pixels => {
|
|
240
|
+
const canvas = document.createElement('canvas');
|
|
241
|
+
const ctx = canvas.getContext('2d');
|
|
242
|
+
const { width, height } = texture.getSize();
|
|
243
|
+
|
|
244
|
+
canvas.width = width;
|
|
245
|
+
canvas.height = height;
|
|
246
|
+
|
|
247
|
+
const img = new Image();
|
|
248
|
+
const imageData = ctx.createImageData(width, height);
|
|
249
|
+
imageData.data.set(new Uint8ClampedArray(pixels));
|
|
250
|
+
|
|
251
|
+
ctx.putImageData(imageData, 0, 0);
|
|
252
|
+
|
|
253
|
+
img.onload = () => {
|
|
254
|
+
resolve(img);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
img.onerror = event => {
|
|
258
|
+
console.log(`Error loading image: ${textureName}`);
|
|
259
|
+
reject(event);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
img.src = canvas.toDataURL();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const dataUrlToBlob = dataURI => {
|
|
270
|
+
// convert base64 to raw binary data held in a string
|
|
271
|
+
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
|
272
|
+
const byteString = atob(dataURI.split(',')[1]);
|
|
273
|
+
|
|
274
|
+
// separate out the mime component
|
|
275
|
+
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
|
276
|
+
|
|
277
|
+
// write the bytes of the string to an ArrayBuffer
|
|
278
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
279
|
+
|
|
280
|
+
// create a view into the buffer
|
|
281
|
+
const ia = new Uint8Array(ab);
|
|
282
|
+
|
|
283
|
+
// set the bytes of the buffer to the correct values
|
|
284
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
285
|
+
ia[i] = byteString.charCodeAt(i);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// write the ArrayBuffer to a blob, and you're done
|
|
289
|
+
const blob = new Blob([ ab ], { type: mimeString });
|
|
290
|
+
return blob;
|
|
291
|
+
};
|
|
292
|
+
|
|
180
293
|
export const updateMaterial = inboundData => {
|
|
181
294
|
const { payload } = inboundData;
|
|
182
295
|
const {
|
|
@@ -194,6 +307,7 @@ export const updateMaterial = inboundData => {
|
|
|
194
307
|
metallic,
|
|
195
308
|
metallicTexture,
|
|
196
309
|
roughness,
|
|
310
|
+
roughnessTexture,
|
|
197
311
|
microSurfaceTexture,
|
|
198
312
|
emissiveColor,
|
|
199
313
|
emissiveIntensity,
|
|
@@ -293,10 +407,26 @@ export const updateMaterial = inboundData => {
|
|
|
293
407
|
// Metallic
|
|
294
408
|
if (metallic !== undefined) material.metallic = metallic;
|
|
295
409
|
if (!_.isEmpty(metallicTexture?.src || metallicTexture?.url)) {
|
|
296
|
-
|
|
297
|
-
|
|
410
|
+
// Metallic Texture uses the B channel of the metallicTexture for a PBRMaterial
|
|
411
|
+
const texture = newTexture(metallicTexture?.src || metallicTexture?.url);
|
|
412
|
+
let currentTexture = material.metallicTexture;
|
|
413
|
+
if (!currentTexture) {
|
|
414
|
+
material.metallicTexture = newTexture();
|
|
415
|
+
currentTexture = material.metallicTexture;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const metallicBlob = dataUrlToBlob(texture.url);
|
|
298
419
|
|
|
299
|
-
|
|
420
|
+
Promise.all([ loadImage(metallicBlob), textureToImage(currentTexture, 'metallic') ])
|
|
421
|
+
.then(data => {
|
|
422
|
+
const [ metallicImage, currentImage ] = data;
|
|
423
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, currentImage);
|
|
424
|
+
currentTexture.updateURL(combinedTexture);
|
|
425
|
+
|
|
426
|
+
texture.dispose();
|
|
427
|
+
applyUVSettings({ texture: material.metallicTexture, material });
|
|
428
|
+
})
|
|
429
|
+
.catch(() => console.log(`Error updating the metallic texture of material: ${material.name}`));
|
|
300
430
|
}
|
|
301
431
|
|
|
302
432
|
// Roughness
|
|
@@ -308,6 +438,30 @@ export const updateMaterial = inboundData => {
|
|
|
308
438
|
applyUVSettings({ texture: material.microSurfaceTexture, material });
|
|
309
439
|
}
|
|
310
440
|
|
|
441
|
+
if (!_.isEmpty(roughnessTexture?.src || roughnessTexture?.url)) {
|
|
442
|
+
// Roughness Texture uses the G channel of the metallicTexture for a PBRMaterial
|
|
443
|
+
const texture = newTexture(roughnessTexture?.src || roughnessTexture?.url);
|
|
444
|
+
let currentTexture = material.metallicTexture;
|
|
445
|
+
if (!currentTexture) {
|
|
446
|
+
material.metallicTexture = newTexture();
|
|
447
|
+
currentTexture = material.metallicTexture;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const roughnessBlob = dataUrlToBlob(texture.url);
|
|
451
|
+
|
|
452
|
+
Promise.all([ loadImage(roughnessBlob), textureToImage(currentTexture, 'metallic') ])
|
|
453
|
+
.then(data => {
|
|
454
|
+
const [ roughnessImage, metallicImage ] = data;
|
|
455
|
+
const combinedTexture = combineMetallicRoughnessTextures(metallicImage, roughnessImage);
|
|
456
|
+
currentTexture.updateURL(combinedTexture);
|
|
457
|
+
|
|
458
|
+
texture.dispose();
|
|
459
|
+
applyUVSettings({ texture: material.metallicTexture, material });
|
|
460
|
+
})
|
|
461
|
+
.catch(() => console.log(`Error updating the roughness texture of material: ${material.name}`));
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
311
465
|
// Emissive
|
|
312
466
|
if (emissiveColor) material.emissiveColor = newColor(emissiveColor);
|
|
313
467
|
if (emissiveIntensity !== undefined) material.emissiveIntensity = emissiveIntensity;
|
|
@@ -948,4 +1102,4 @@ export const updatePublish = inboundData => {
|
|
|
948
1102
|
};
|
|
949
1103
|
|
|
950
1104
|
newMetaDataEntry('publish', withOptimizationValues);
|
|
951
|
-
};
|
|
1105
|
+
};
|
|
@@ -341,7 +341,7 @@ export const buildSelectedMaterialArray = () => {
|
|
|
341
341
|
return [ mainMaterial ];
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
const arr = selectedMeshes
|
|
344
|
+
const arr = selectedMeshes?.map?.(mesh => {
|
|
345
345
|
const material = mesh.material;
|
|
346
346
|
if (material) {
|
|
347
347
|
const existingMaterial = scene.metadata.materials?.find(mat => mat.materialId === material.id);
|
|
@@ -525,29 +525,34 @@ export const getExistingUVSettings = args => {
|
|
|
525
525
|
const userMaterials = getUserMaterials();
|
|
526
526
|
const material = userMaterials.find(mat => mat.id === materialId);
|
|
527
527
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
528
|
+
if (material) {
|
|
529
|
+
Object.keys(material)?.find?.(key => {
|
|
530
|
+
const validMaterialWithTexture = _.isObject(material[key])
|
|
531
|
+
&& !_.isArray(material[key])
|
|
532
|
+
&& key.endsWith('Texture');
|
|
533
|
+
|
|
534
|
+
const texture = material[key];
|
|
535
|
+
const isEnvironment = key.toLowerCase().includes('environment');
|
|
536
|
+
const hasAllKeys = texture && [ 'uAng', 'wAng', 'vAng', 'uOffset', 'vOffset', 'uScale', 'vScale' ]
|
|
537
|
+
.every(key => texture[key] !== undefined);
|
|
538
|
+
|
|
539
|
+
// NOTE: All UV's are adjusted globally, at the same time in canvasUpdateHelpers.js
|
|
540
|
+
// when **any** UV setting is changed. So we only need to find the first one to
|
|
541
|
+
// restore the UV settings.
|
|
542
|
+
if (!isEnvironment && validMaterialWithTexture && hasAllKeys && !settingsFound) {
|
|
543
|
+
settingsFound = true;
|
|
544
|
+
uvSettings = {
|
|
545
|
+
uvXScale: texture.uScale,
|
|
546
|
+
uvYScale: texture.vScale,
|
|
547
|
+
uvXRotation: texture.uAng,
|
|
548
|
+
uvYRotation: texture.wAng,
|
|
549
|
+
uvZRotation: texture.vAng,
|
|
550
|
+
uvXOffset: texture.uOffset,
|
|
551
|
+
uvYOffset: texture.vOffset,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
551
556
|
|
|
552
557
|
return uvSettings;
|
|
553
558
|
};
|
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,24 @@ 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({ name: file.name, src: reader.result });
|
|
147
|
+
};
|
|
130
148
|
|
|
131
149
|
useEffect(() => {
|
|
132
150
|
getPreviewURL();
|
|
@@ -189,6 +207,8 @@ const App = () => {
|
|
|
189
207
|
Import material
|
|
190
208
|
</Button>
|
|
191
209
|
|
|
210
|
+
<input type={'file'} onChange={handleUpload} accept={'image/*'} />
|
|
211
|
+
|
|
192
212
|
<Button
|
|
193
213
|
theme={theme}
|
|
194
214
|
$selectedTheme={selectedTheme}
|
|
@@ -249,4 +269,4 @@ const App = () => {
|
|
|
249
269
|
);
|
|
250
270
|
};
|
|
251
271
|
|
|
252
|
-
export default App;
|
|
272
|
+
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';
|