@archvisioninc/canvas 3.3.8 → 3.3.9

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.
@@ -0,0 +1,269 @@
1
+ import * as BABYLON from 'babylonjs';
2
+
3
+ // GLTFFileLoader needs to be declared for the GLTF loader to work -- even if unused.
4
+ // eslint-disable-next-line no-unused-vars
5
+ import { GLTFFileLoader } from 'babylonjs-loaders';
6
+ import { reactProps as props } from '../Canvas';
7
+ import { DRAG_DROP_FORMATS } from '../enums';
8
+ import { canvas, engine, scene, initSceneFromFile, blobToGLB, getUserMaterials, getUserTextures, getUserMeshes } from '../helpers';
9
+ import { MESSAGE_TYPES, TEXTURE_SIZE_THRESHOLD, MATERIAL_COUNT_THRESHOLD, MESH_COUNT_THRESHOLD } from '../constants';
10
+ import _ from 'lodash';
11
+ import { modelToOrigin } from '../actions';
12
+ const {
13
+ REACT_APP_NAME: name
14
+ } = process.env;
15
+ const inCanvasIDE = name === '@archvisioninc/canvas';
16
+ const clearExisting = true;
17
+ export const newEngine = async () => {
18
+ const webGLSupported = () => {
19
+ try {
20
+ const canvas = document.createElement('canvas');
21
+ return !!window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
22
+ } catch (e) {
23
+ return false;
24
+ }
25
+ };
26
+ if (webGLSupported()) {
27
+ const webGLEngine = () => new BABYLON.Engine(canvas, true);
28
+ const enableWebGPU = false;
29
+ try {
30
+ const browserSupported = await BABYLON.WebGPUEngine.IsSupportedAsync;
31
+ if (browserSupported && enableWebGPU) {
32
+ const webGPUEngine = new BABYLON.WebGPUEngine(canvas);
33
+ await webGPUEngine.initAsync(canvas);
34
+ return webGPUEngine;
35
+ }
36
+ return webGLEngine();
37
+ } catch (e) {
38
+ console.error(e);
39
+ return webGLEngine();
40
+ }
41
+ }
42
+ return new BABYLON.NullEngine();
43
+ };
44
+ export const newScene = () => new BABYLON.Scene(engine);
45
+ export const checkForLargeTextures = () => {
46
+ const userTextures = getUserTextures();
47
+ const largeTexture = userTextures.find(texture => {
48
+ const size = texture.getSize();
49
+ const isLargeTexture = size?.width >= TEXTURE_SIZE_THRESHOLD || size?.height >= TEXTURE_SIZE_THRESHOLD;
50
+ if (isLargeTexture) return texture;
51
+ });
52
+ return largeTexture;
53
+ };
54
+ export const warningChecks = () => {
55
+ const hasLargeFile = checkForLargeTextures();
56
+ const manyMaterials = getUserMaterials().length > MATERIAL_COUNT_THRESHOLD;
57
+ const manyMeshes = getUserMeshes().length > MESH_COUNT_THRESHOLD;
58
+ const messages = [];
59
+ const config = {
60
+ type: MESSAGE_TYPES.warning,
61
+ closeButton: true,
62
+ timer: 4
63
+ };
64
+ if (hasLargeFile) {
65
+ const message = `Large textures at or above ${TEXTURE_SIZE_THRESHOLD}, performance may be affected.`;
66
+ messages.push({
67
+ message,
68
+ config
69
+ });
70
+ }
71
+ if (manyMaterials) {
72
+ const message = 'High material count, performance may be affected.';
73
+ messages.push({
74
+ message,
75
+ config
76
+ });
77
+ }
78
+ if (manyMeshes) {
79
+ const message = 'High mesh count, shadows disabled for performance. Reenable in environments panel.';
80
+ const directionalLight = scene.getLightById('directional');
81
+ directionalLight.shadowEnabled = false;
82
+ messages.push({
83
+ message,
84
+ config
85
+ });
86
+ }
87
+ props.addNotification?.(messages, clearExisting);
88
+ };
89
+ const checkSupportedFileTypes = files => {
90
+ const config = {
91
+ type: MESSAGE_TYPES.error
92
+ };
93
+ let hasPrimaryFile = false;
94
+ if (files) {
95
+ for (let i = 0; i < files.length; i++) {
96
+ const file = files[i];
97
+ const workingExtension = file.name.split('.').pop() || '';
98
+ if (!hasPrimaryFile && DRAG_DROP_FORMATS.includes(workingExtension)) {
99
+ hasPrimaryFile = true;
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ if (!hasPrimaryFile) {
105
+ const message = 'Please upload a .gltf or .glb file.';
106
+ props.addNotification?.([{
107
+ message,
108
+ config
109
+ }], clearExisting);
110
+ }
111
+ };
112
+ export const progressConfig = (currentStep, bytesLoaded, bytesTotal) => {
113
+ const config = {
114
+ type: MESSAGE_TYPES.loading,
115
+ showModal: (inCanvasIDE || props.previewMode) && !props.integration ? false : true,
116
+ currentStep,
117
+ closeButton: false,
118
+ totalSteps: 2,
119
+ bytesLoaded,
120
+ bytesTotal
121
+ };
122
+ return config;
123
+ };
124
+ const onProgress = (e, args) => {
125
+ const {
126
+ addNotification
127
+ } = args;
128
+ const loaded = e.loaded;
129
+ const total = e.total || loaded;
130
+ const progress = (bytes, totalBytes) => {
131
+ const conversionUnit = 1024;
132
+ const unit = value => {
133
+ const inKB = value / conversionUnit;
134
+ const inMB = inKB / conversionUnit;
135
+ const inGB = inMB / conversionUnit;
136
+ const isGB = inMB >= conversionUnit;
137
+ const raw = isGB ? inGB : inMB;
138
+ const label = isGB ? 'GB' : 'MB';
139
+ return {
140
+ raw,
141
+ label
142
+ };
143
+ };
144
+ const label = unit(bytes).label;
145
+ const total = parseFloat(totalBytes || 0);
146
+ const stringOptions = {
147
+ minimumFractionDigits: 2,
148
+ maximumFractionDigits: 2
149
+ };
150
+ const size = `${unit(bytes).raw?.toLocaleString('en-US', stringOptions)} ${label}`;
151
+ const percent = `${Math.floor(bytes / total * 100)}%`;
152
+ const finalSize = `${unit(total).raw.toLocaleString('en-US', stringOptions)} ${unit(total).label}`;
153
+ const both = `${finalSize} ${percent}`;
154
+ return {
155
+ size,
156
+ percent,
157
+ both
158
+ };
159
+ };
160
+ const message = {
161
+ message: props.previewMode ? 'Building preview...' : `Loading... ${progress(loaded, total).both}`,
162
+ config: progressConfig(2, loaded, total)
163
+ };
164
+ addNotification?.([message], clearExisting);
165
+ };
166
+ export const loadFromGuidURL = url => {
167
+ try {
168
+ if (!url) throw new Error('No URL provided');
169
+ const sceneLoader = BABYLON.SceneLoader;
170
+ const handleProgress = e => onProgress(e, {
171
+ addNotification: props.addNotification
172
+ });
173
+ sceneLoader.ShowLoadingScreen = false;
174
+ sceneLoader.LoadAsync(url, null, engine, handleProgress).then(sceneData => {
175
+ props.clearNotifications?.();
176
+ initSceneFromFile(sceneData);
177
+ });
178
+ } catch (e) {
179
+ console.error(e);
180
+ props.addNotification?.([{
181
+ message: e,
182
+ config: {
183
+ type: MESSAGE_TYPES.error
184
+ }
185
+ }], clearExisting);
186
+ }
187
+ };
188
+ export const loadFromBlob = blob => {
189
+ try {
190
+ if (!blob) throw new Error('No file provided');
191
+ const file = blobToGLB(blob, 'downloadedModel');
192
+ const sceneLoader = BABYLON.SceneLoader;
193
+ const handleProgress = e => onProgress(e, {
194
+ addNotification: props.addNotification
195
+ });
196
+ sceneLoader.ShowLoadingScreen = false;
197
+ sceneLoader.LoadAsync('file:', file, engine, handleProgress).then(sceneData => {
198
+ props.clearNotifications?.();
199
+ initSceneFromFile(sceneData);
200
+ });
201
+ } catch (e) {
202
+ console.error(e);
203
+ props.addNotification?.([{
204
+ message: e,
205
+ config: {
206
+ type: MESSAGE_TYPES.error
207
+ }
208
+ }], clearExisting);
209
+ }
210
+ };
211
+ let filesInput;
212
+ export const loadFromDragDrop = () => {
213
+ const handleCloseLoading = () => props.clearNotifications?.();
214
+ const sceneLoaded = (file, newScene) => {
215
+ const fileName = file.name.split('.').shift();
216
+ initSceneFromFile(newScene, fileName);
217
+ modelToOrigin();
218
+ };
219
+ const onProgress = null;
220
+ const additionalRenderLoopLogic = null;
221
+ const textureLoading = null;
222
+ const startingProcessingFiles = () => {
223
+ const hasGuid = !_.isEmpty(props.guidURL);
224
+ if (hasGuid) {
225
+ // eslint-disable-next-line max-len
226
+ const message = "This model has published data that can't be updated from a drag and drop operation. Use Create from gLTF to start a new file.";
227
+ const config = {
228
+ type: MESSAGE_TYPES.warning
229
+ };
230
+ props.addNotification?.([{
231
+ message,
232
+ config
233
+ }], clearExisting);
234
+ throw new Error(message);
235
+ }
236
+ checkSupportedFileTypes();
237
+ };
238
+ const onReload = null;
239
+ const onError = (file, scene, error) => {
240
+ const missingBin = error.includes('.bin');
241
+ const missingTextures = error.includes('textures/');
242
+ const missingMessage = `Missing ${missingTextures ? 'textures folder' : '.bin file'}.`;
243
+ const message = missingBin || missingTextures ? missingMessage : error;
244
+ const config = {
245
+ type: MESSAGE_TYPES.error
246
+ };
247
+ handleCloseLoading();
248
+ props.addNotification?.([{
249
+ message,
250
+ config
251
+ }], clearExisting);
252
+ };
253
+ const handleDisplayLoading = () => {
254
+ const message = 'Importing...';
255
+ const config = {
256
+ type: MESSAGE_TYPES.loading,
257
+ showModal: inCanvasIDE || props.previewMode ? false : true
258
+ };
259
+ props.addNotification?.([{
260
+ message,
261
+ config
262
+ }], clearExisting);
263
+ };
264
+ BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = handleDisplayLoading;
265
+ BABYLON.DefaultLoadingScreen.prototype.hideLoadingUI = _.noop;
266
+ if (filesInput) filesInput.dispose();
267
+ filesInput = new BABYLON.FilesInput(engine, scene, sceneLoaded, onProgress, additionalRenderLoopLogic, textureLoading, startingProcessingFiles, onReload, onError);
268
+ filesInput.monitorElementForDragNDrop(canvas);
269
+ };
@@ -0,0 +1,34 @@
1
+ import * as BABYLON from 'babylonjs';
2
+ import * as MATERIALS from 'babylonjs-materials';
3
+ import { scene, newColor } from '../helpers';
4
+ import { exclusionMaterials } from '../enums';
5
+ export const getUserMaterials = () => {
6
+ const materials = scene.materials.filter(material => !exclusionMaterials.includes(material.id));
7
+ return materials;
8
+ };
9
+ export const getUserTextures = () => {
10
+ const userMaterials = getUserMaterials();
11
+ const userTextures = userMaterials.map(material => material.getActiveTextures());
12
+ return userTextures.flat();
13
+ };
14
+ export const newPBRMaterial = name => {
15
+ const defaultName = `pbrMaterial_${getUserMaterials().length + 1}`;
16
+ return new BABYLON.PBRMaterial(name || defaultName, scene);
17
+ };
18
+ export const newStandardMaterial = name => {
19
+ const defaultName = `standardMaterial_${getUserMaterials().length + 1}`;
20
+ return new BABYLON.StandardMaterial(name || defaultName, scene);
21
+ };
22
+ export const newGridMaterial = name => {
23
+ const gridMaterialName = name || `grid_${getUserMaterials().length}`;
24
+ return new MATERIALS.GridMaterial(gridMaterialName, scene);
25
+ };
26
+ export const setDefaultGridProperties = material => {
27
+ material.majorUnitFrequency = 1;
28
+ material.minorUnitVisibility = 0.5;
29
+ material.gridRatio = 1;
30
+ material.backFaceCulling = false;
31
+ material.mainColor = newColor(0.25, 0.27, 0.33);
32
+ material.lineColor = newColor(0.33, 0.35, 0.42);
33
+ material.opacity = 0.95;
34
+ };
@@ -0,0 +1,169 @@
1
+ import * as BABYLON from 'babylonjs';
2
+ import { scene, selectedMeshes, newVector, getRadius, getMidpoint, canvas } from '../helpers';
3
+ import { exclusionMeshes, exclusionTNodes, scaleUnits } from '../enums';
4
+ import { MESH_PARAMS } from '../constants';
5
+ import _ from 'lodash';
6
+ export const getUserNodes = () => {
7
+ const userNodes = scene.transformNodes.filter(node => {
8
+ const validNode = !exclusionTNodes.includes(node.id);
9
+ if (validNode) return node;
10
+ });
11
+ return userNodes;
12
+ };
13
+ export const getUserMeshes = () => {
14
+ const meshes = scene.meshes.filter(mesh => {
15
+ const validMesh = !exclusionMeshes.includes(mesh.id) && mesh.subMeshes;
16
+ if (validMesh) return mesh;
17
+ });
18
+ return meshes;
19
+ };
20
+ export const newGround = (name, groundOptions = {}) => {
21
+ const groundName = name || `ground_${getUserMeshes().length + 1}`;
22
+ return BABYLON.MeshBuilder.CreateGround(groundName, groundOptions, scene);
23
+ };
24
+ export const getScaleFactor = sourceUnit => {
25
+ const unit = _.toLower(sourceUnit);
26
+ const validUnit = sourceUnit && scaleUnits.find(item => item.name === unit || item.abbreviation === unit);
27
+ return validUnit ? validUnit.scaleFactor : 1;
28
+ };
29
+ export const getBoundingMeshData = meshArray => {
30
+ const meshes = meshArray || getUserMeshes();
31
+ const singleSelection = meshes.length === 1;
32
+ const singleBox = singleSelection && meshes[0]?.getBoundingInfo?.().boundingBox;
33
+ const scaling = singleSelection && meshes[0].scaling;
34
+ const rotation = singleSelection && meshes[0].rotation;
35
+ let minimum = newVector(0, 0, 0);
36
+ let maximum = newVector(0, 0, 0);
37
+ for (let i = 0; i < meshes.length; i++) {
38
+ const boundingInfo = meshes[i]?.getBoundingInfo?.();
39
+ if (boundingInfo) {
40
+ const workingBoundingBox = boundingInfo.boundingBox;
41
+ if (i === 0) {
42
+ maximum = workingBoundingBox.maximumWorld;
43
+ minimum = workingBoundingBox.minimumWorld;
44
+ continue;
45
+ }
46
+ maximum = maximum.maximizeInPlace(workingBoundingBox.maximumWorld);
47
+ minimum = minimum.minimizeInPlace(workingBoundingBox.minimumWorld);
48
+ }
49
+ }
50
+ const boundingBox = {
51
+ ...(singleBox || {}),
52
+ center: getMidpoint(minimum, maximum),
53
+ minimum,
54
+ maximum,
55
+ radius: getRadius(minimum, maximum),
56
+ scaling: scaling || newVector(1, 1, 1),
57
+ rotation: rotation || newVector(0, 0, 0)
58
+ };
59
+ if (!_.isEmpty(boundingBox)) return boundingBox;
60
+ };
61
+ export const getBoundingDistance = () => {
62
+ const sceneBoundingMesh = getBoundingMeshData();
63
+ if (sceneBoundingMesh) {
64
+ const boundingBox = sceneBoundingMesh;
65
+ const min = boundingBox.minimum;
66
+ const max = boundingBox.maximum;
67
+ const xDelta = Math.abs(max._x - min._x);
68
+ const yDelta = Math.abs(max._y - min._y);
69
+ const zDelta = Math.abs(max._z - min._z);
70
+ const distance = Math.sqrt(Math.pow(xDelta, 2) + Math.pow(yDelta, 2) + Math.pow(zDelta, 2));
71
+ return distance;
72
+ }
73
+ };
74
+ export const getParamOfSelectedMeshes = parameter => {
75
+ const meshes = selectedMeshes.length > 0 ? selectedMeshes : getUserMeshes();
76
+ const boundingBox = getBoundingMeshData(meshes);
77
+ if (boundingBox) {
78
+ const {
79
+ center,
80
+ radius,
81
+ rotation,
82
+ scaling,
83
+ centerWorld
84
+ } = boundingBox;
85
+ switch (parameter) {
86
+ case MESH_PARAMS.rotation:
87
+ return rotation;
88
+ case MESH_PARAMS.scaling:
89
+ return scaling;
90
+ case MESH_PARAMS.radius:
91
+ return radius;
92
+ default:
93
+ return centerWorld || center;
94
+ }
95
+ }
96
+ };
97
+ export const getMeshVertices = (meshes, kind = BABYLON.VertexBuffer.PositionKind) => {
98
+ const groupedVertices = [];
99
+ meshes.forEach(mesh => {
100
+ const buffer = mesh.getVerticesData(kind);
101
+ const vertices = [];
102
+ for (let i = 0; i < buffer.length; i += 3) {
103
+ const vertex = newVector(buffer[i], buffer[i + 1], buffer[i + 2]);
104
+ vertices.push(vertex);
105
+ }
106
+ groupedVertices.push({
107
+ mesh: mesh.id,
108
+ vertices
109
+ });
110
+ });
111
+ return groupedVertices;
112
+ };
113
+ export const getWorldPositionsByMesh = (meshName, vertices) => {
114
+ const mesh = scene.getMeshById(meshName);
115
+ const meshWorldMatrix = mesh.getWorldMatrix(true);
116
+ const positionArray = vertices.map(vertex => {
117
+ return BABYLON.Vector3.TransformCoordinates(vertex, meshWorldMatrix);
118
+ });
119
+ return positionArray;
120
+ };
121
+
122
+ // Below is from https://forum.unity.com/threads/calculating-world-to-screen-projection-manully.1197199/
123
+ // It is converted to work specifically with BabylonJS
124
+
125
+ const WorldToLocal = (aCamPos, aCamRot, aPos) => {
126
+ const inverse = BABYLON.Quaternion.Inverse(aCamRot);
127
+ const camToVertex = aPos.subtract(aCamPos);
128
+ const toLocal = camToVertex.applyRotationQuaternion(inverse);
129
+ return toLocal;
130
+ };
131
+ const Project = (aPos, aFov, aAspect) => {
132
+ let f = 1 / Math.tan(aFov * 0.5);
133
+ f = f / aPos.z;
134
+ aPos.x *= f / aAspect;
135
+ aPos.y *= f;
136
+ return aPos;
137
+ };
138
+ const ClipSpaceToViewport = aPos => {
139
+ aPos.x = aPos.x * 0.5 + 0.5;
140
+ aPos.y = aPos.y * 0.5 + 0.5;
141
+ return aPos;
142
+ };
143
+ const WorldToViewport = (aCamPos, aCamRot, aFov, aAspect, aPos) => {
144
+ let p = WorldToLocal(aCamPos, aCamRot, aPos);
145
+ p = Project(p, aFov, aAspect);
146
+ return ClipSpaceToViewport(p);
147
+ };
148
+ const WorldToScreenPos = (aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos) => {
149
+ const p = WorldToViewport(aCamPos, aCamRot, aFov, aScrWidth / aScrHeight, aPos);
150
+ p.x *= aScrWidth;
151
+ p.y *= aScrHeight;
152
+ return p;
153
+ };
154
+ export const WorldToGUIPos = (aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos) => {
155
+ const xScale = 1 / aScrWidth;
156
+ const yScale = 1 / aScrHeight;
157
+ let p = WorldToScreenPos(aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos);
158
+ p.y = aScrHeight - p.y;
159
+ p = p.multiplyByFloats(xScale, yScale, 1);
160
+ return p;
161
+ };
162
+ export const getScaleToZeroOne = vertices => {
163
+ const scaledVertices = vertices.map(vertex => {
164
+ const xScale = 1 / canvas.width;
165
+ const yScale = 1 / canvas.height;
166
+ return vertex.multiplyByFloats(xScale, yScale, 1);
167
+ });
168
+ return scaledVertices;
169
+ };
@@ -0,0 +1,11 @@
1
+ import * as BABYLON from 'babylonjs';
2
+ import { scene, newColor } from '../helpers';
3
+ const debug = false;
4
+ export const newPickingRay = (x, y) => {
5
+ const ray = scene.createPickingRay(x, y, BABYLON.Matrix.Identity(), scene.activeCamera);
6
+ const rayHelper = new BABYLON.RayHelper(ray);
7
+ if (debug) {
8
+ rayHelper.show(scene, newColor('#359eff'));
9
+ }
10
+ return rayHelper;
11
+ };
@@ -0,0 +1,35 @@
1
+ import { LISTENERS } from '../constants';
2
+ import { serializeScene, shortcutArray } from '../helpers';
3
+ import { reactProps as props } from '../Canvas';
4
+ import _ from 'lodash';
5
+ let initiatedListeners = [];
6
+ const actionFunction = args => {
7
+ const {
8
+ event,
9
+ shortcutArray
10
+ } = args;
11
+ const currentShortcut = shortcutArray.find(shortcut => shortcut.key === event.key);
12
+ const isInput = event?.target.localName === 'input';
13
+ if (!_.isEmpty(currentShortcut) && !isInput) {
14
+ event.preventDefault();
15
+ currentShortcut.action(event);
16
+ props.setSerializedData?.(serializeScene());
17
+ }
18
+ };
19
+ const shortcutListener = e => actionFunction({
20
+ event: e,
21
+ shortcutArray
22
+ });
23
+ export const handleShortcutListeners = remove => {
24
+ const keyup = LISTENERS.keyup;
25
+ if (remove) {
26
+ if (initiatedListeners.includes(keyup)) {
27
+ window.removeEventListener(keyup, shortcutListener);
28
+ initiatedListeners = [];
29
+ }
30
+ return;
31
+ }
32
+ if (initiatedListeners.includes(keyup)) return;
33
+ window.addEventListener(keyup, shortcutListener);
34
+ initiatedListeners.push(keyup);
35
+ };