@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.
- package/dist/Canvas.js +67 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/shortcutActions.js +313 -0
- package/dist/constants/constants.js +80 -0
- package/dist/constants/index.js +1 -0
- package/dist/enums/aspectRatios.js +17 -0
- package/dist/enums/dimensions.js +20 -0
- package/dist/enums/downscaling.js +16 -0
- package/dist/enums/exclusions.js +4 -0
- package/dist/enums/formats.js +1 -0
- package/dist/enums/index.js +8 -0
- package/dist/enums/orthoOptions.js +28 -0
- package/dist/enums/scaleUnits.js +25 -0
- package/dist/enums/shortcuts.js +89 -0
- package/dist/helpers/cameraHelpers.js +86 -0
- package/dist/helpers/canvasAddHelpers.js +161 -0
- package/dist/helpers/canvasCommunicationHelpers.js +52 -0
- package/dist/helpers/canvasRemoveHelpers.js +230 -0
- package/dist/helpers/canvasUpdateHelpers.js +1365 -0
- package/dist/helpers/gizmoHelpers.js +156 -0
- package/dist/helpers/guiHelpers.js +46 -0
- package/dist/helpers/index.js +16 -0
- package/dist/helpers/initHelpers.js +513 -0
- package/dist/helpers/lightHelpers.js +17 -0
- package/dist/helpers/loadHelpers.js +269 -0
- package/dist/helpers/materialHelpers.js +34 -0
- package/dist/helpers/meshHelpers.js +169 -0
- package/dist/helpers/rayHelpers.js +11 -0
- package/dist/helpers/shortcutHelpers.js +35 -0
- package/dist/helpers/utilityHelpers.js +710 -0
- package/dist/helpers/viewportHelpers.js +364 -0
- package/dist/styles.js +25 -0
- package/package.json +2 -1
|
@@ -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
|
+
};
|