1 | ;
|
2 |
|
3 | import * as THREE from 'three';
|
4 | import EdgesHelper from './EdgesHelper.js';
|
5 | import FluxRenderer from './FluxRenderer.js';
|
6 | import FluxCameras from './FluxCameras.js';
|
7 | import * as FluxJsonToThree from 'flux-json-to-three';
|
8 | import {scene} from 'flux-modelingjs';
|
9 | import * as constants from './controls/constants.js';
|
10 | import * as print from './utils/debugPrint.js';
|
11 |
|
12 | /**
|
13 | * UI widget to render 3D geometry.
|
14 | * This class provides all the interface you need to add the flux web
|
15 | * viewer to your SDK app. This allows you to interpret Flux json data
|
16 | * and render that geometry. You may also create any number of viewports
|
17 | * and they will share the finite number of WebGL contexts available
|
18 | * from the browser.<br>
|
19 | * The most commonly used functions are <a href="#setGeometryEntity">setGeometryEntity</a> (to set geometry to render)
|
20 | * and <a href="#.isKnownGeom">isKnownGeom</a> (determine if JSON is geometry) so you might want to start reading there. <br>
|
21 | * Note: If you are using <a href="https://community.flux.io/content/kbentry/2718/materials-1.html">Flux materials</a> that have the parameter roughness
|
22 | * set then you will need to configure your server to have a <a href="https://content-security-policy.com">content security
|
23 | * policy</a> that allows content from https://object-library.storage.googleapis.com
|
24 | * so that our environment texture images can be loaded.
|
25 | * @class FluxViewport
|
26 | * @param {Element} domParent The div container for the canvas
|
27 | * @param {Object} optionalParams Object containing all other parameters
|
28 | * @param {Number} optionalParams.width The width of the canvas
|
29 | * @param {Number} optionalParams.height The height of the canvas
|
30 | * @param {String} optionalParams.tessUrl The url for making brep tessellation requests (overrides projectId) (Used when server is not flux.io)
|
31 | * @param {Enumeration} optionalParams.selection Whether to enable user selection
|
32 | * @param {String} optionalParams.projectId Id of a flux project (required to render breps)
|
33 | * @param {String} optionalParams.token The current flux auth token (required to render breps)
|
34 | */
|
35 | export default function FluxViewport (domParent, optionalParams) {
|
36 | var width;
|
37 | var height;
|
38 | var tessUrl;
|
39 | var token;
|
40 | var selection = constants.Selection.NONE;
|
41 | if (optionalParams) {
|
42 | width = optionalParams.width;
|
43 | height = optionalParams.height;
|
44 | if (optionalParams.tessUrl) {
|
45 | tessUrl = optionalParams.tessUrl;
|
46 | } else if (optionalParams.projectId) {
|
47 | tessUrl = _getTessUrl(optionalParams.projectId);
|
48 | }
|
49 | token = optionalParams.token;
|
50 | if (optionalParams.selection) {
|
51 | selection = optionalParams.selection;
|
52 | }
|
53 | }
|
54 |
|
55 | var renderWidth = 100;//px
|
56 | if (width == null) {
|
57 | renderWidth = domParent.clientWidth;
|
58 | } else {
|
59 | renderWidth = Math.max(renderWidth, width);
|
60 | }
|
61 |
|
62 | var renderHeight = 100;//px
|
63 | if (height == null) {
|
64 | renderHeight = domParent.clientHeight;
|
65 | } else {
|
66 | renderHeight = Math.max(renderHeight, height);
|
67 | }
|
68 |
|
69 | if (!domParent) {
|
70 | throw new Error('domParent must be specified to FluxViewport');
|
71 | }
|
72 |
|
73 | this._sceneBuilder = new FluxJsonToThree.SceneBuilder(tessUrl, token);
|
74 | // Only allow merging when selection is disabled
|
75 | this._sceneBuilder.setAllowMerge(constants.Selection.NONE === selection);
|
76 |
|
77 | this._renderer = new FluxRenderer(domParent, renderWidth, renderHeight, selection);
|
78 | this._initCallback();
|
79 |
|
80 | // Make sure to render on mouse over in case the renderer has swapped contexts
|
81 | var _this = this;
|
82 | domParent.addEventListener('mouseenter', function(){
|
83 | _this.render();
|
84 | });
|
85 |
|
86 | // Cache of the Flux entity objects for downloading
|
87 | this._entities = null;
|
88 |
|
89 | this._latestSceneResults = null;
|
90 |
|
91 | // Track the last blob that was downloaded for memory cleanup
|
92 | this._downloadUrl = null;
|
93 |
|
94 | // Whether the viewport is locked on the current geometry and will automatically focus on new geometry when updating the entities
|
95 | this._autoFocus = true;
|
96 |
|
97 | // Track whether geometry is being converted, so we don't try two at once
|
98 | this.running = false;
|
99 | }
|
100 |
|
101 | FluxViewport.prototype = Object.create( THREE.EventDispatcher.prototype );
|
102 | FluxViewport.prototype.constructor = FluxViewport;
|
103 |
|
104 | /**
|
105 | * Build the url to use for tessellation requests to flux.
|
106 | * @private
|
107 | * @param {String} projectId Hashed project id to use in url
|
108 | * @return {String} Url to use for tessellation requests
|
109 | */
|
110 | function _getTessUrl(projectId) {
|
111 | return 'https://flux.io/p/'+projectId+
|
112 | '/api/blockexec?block=flux-internal/parasolid/Parasolid';
|
113 | }
|
114 |
|
115 | /**
|
116 | * @summary Enumeration of edges rendering modes.
|
117 | * This determines whether edges will be shown when rendering the front face, back face or both.
|
118 | * Front edge rendering can be used to achieve hidden line rendering.<br>
|
119 | * Options are NONE, FRONT, BACK, BOTH
|
120 | * @return {Object} enumeration
|
121 | */
|
122 | FluxViewport.getEdgesModes = function () {
|
123 | return EdgesHelper.EDGES_MODES;
|
124 | };
|
125 |
|
126 | /**
|
127 | * @summary Enumeration of selection modes.
|
128 | * This determines when selection events occur in response to mouse events.
|
129 | * Options are NONE, CLICK, HOVER
|
130 | * @return {Object} enumeration
|
131 | */
|
132 | FluxViewport.getSelectionModes = function () {
|
133 | return constants.Selection;
|
134 | };
|
135 |
|
136 | /**
|
137 | * Name of the event fired when the camera changes
|
138 | *
|
139 | * This can be used to observe those changes and register a callback.<br>
|
140 | * For example:
|
141 | * fluxViewport.addEventListener(FluxViewport.getChangeEvent(), function() {});
|
142 | * @return {String} Event name
|
143 | */
|
144 | FluxViewport.getChangeEvent = function () {
|
145 | return constants.Events.CHANGE;
|
146 | };
|
147 |
|
148 | /**
|
149 | * Enumeration of different event types
|
150 | * This can be used to differentiate events in the EventListener.
|
151 | * fluxViewport.addEventListener(FluxViewport.getChangeEvent(), function(e) {
|
152 | * FluxViewport.getEvents().SELECT === e.event;});
|
153 | * @return {Object} Enumeration object
|
154 | */
|
155 | FluxViewport.getEvents = function () {
|
156 | return constants.Events;
|
157 | };
|
158 |
|
159 | /**
|
160 | * Determines whether the entity or list of entities contains geometry
|
161 | * @param {Array.<Object>|Object} entities Geometry data
|
162 | * @return {Boolean} True for objects/lists containing geometry
|
163 | */
|
164 | FluxViewport.isKnownGeom = function (entities) {
|
165 | return scene.isGeometry(entities);
|
166 | };
|
167 |
|
168 | //---- Class member functions
|
169 |
|
170 | /**
|
171 | * Set up the callback to render when the camera changes
|
172 | * @private
|
173 | */
|
174 | FluxViewport.prototype._initCallback = function() {
|
175 | var _this = this;
|
176 | this._renderer.addEventListener(constants.Events.CHANGE, function(event) {
|
177 | _this.dispatchEvent(event);
|
178 | _this.render();
|
179 | });
|
180 | };
|
181 |
|
182 | /**
|
183 | * Add a new plugin for user interaction controls.
|
184 | * See ViewportControls.js for more information.
|
185 | * @param {ViewportControls} CustomControls A constructor that implements the controls interface.
|
186 | * @return {CustomControls} The new instance
|
187 | */
|
188 | FluxViewport.prototype.addControls = function(CustomControls) {
|
189 | return this._renderer.addControls(CustomControls);
|
190 | };
|
191 |
|
192 |
|
193 | /**
|
194 | * Get an object that maps from id string to Three.Object3D in the current scene
|
195 | * @return {Object} Id to object scene map
|
196 | */
|
197 | FluxViewport.prototype.getObjectMap = function() {
|
198 | return this._latestSceneResults.getObjectMap();
|
199 | };
|
200 |
|
201 | /**
|
202 | * Get the currently selected geometry as a list of id strings
|
203 | * @return {Array.<String>} Current selection
|
204 | */
|
205 | FluxViewport.prototype.getSelection = function() {
|
206 | var selectionMap = this._renderer.getSelection();
|
207 | var keys = Object.keys(selectionMap);
|
208 | var selection = [];
|
209 | for (var i=0;i<keys.length;i++) {
|
210 | var obj = selectionMap[keys[i]];
|
211 | if (obj != null) {
|
212 | selection.push(obj.userData.id);
|
213 | }
|
214 | }
|
215 | return selection;
|
216 | };
|
217 |
|
218 | /**
|
219 | * Define the material that is applied on selected objects
|
220 | * @param {Object} data Flux json description of a material
|
221 | */
|
222 | FluxViewport.prototype.setSelectionMaterial = function(data) {
|
223 | this._renderer.setSelectionMaterial(data);
|
224 | };
|
225 |
|
226 | /**
|
227 | * Set the currently selected geometry
|
228 | * @param {Array.<String>} ids New selection
|
229 | */
|
230 | FluxViewport.prototype.setSelection = function(ids) {
|
231 | if (ids == null || ids.constructor !== Array) return;
|
232 | var map = this._latestSceneResults.getObjectMap();
|
233 | var objects = ids.map(function (id) { return map[id];});
|
234 | this._renderer.setSelection(objects);
|
235 | };
|
236 |
|
237 | /**
|
238 | * Get the JSON representation of the objects with the given ids
|
239 | * @param {Array.<String>} ids List of object ids
|
240 | * @return {Array.<Object>} List of Flux JSON objects
|
241 | */
|
242 | FluxViewport.prototype.getJson = function(ids) {
|
243 | if (ids == null || ids.constructor !== Array) return [];
|
244 | var map = this._latestSceneResults.getObjectMap();
|
245 | return ids.map(function (id) { return map[id].userData.data;});
|
246 | };
|
247 |
|
248 | /**
|
249 | * Actually render the geometry!<br>
|
250 | * This is called automatically when the camera changes.
|
251 | * You may call it on demand as needed when changing properties.
|
252 | * @memberOf FluxViewport
|
253 | */
|
254 | FluxViewport.prototype.render = function() {
|
255 | this._renderer.doRender();
|
256 | };
|
257 |
|
258 | /**
|
259 | * Focus the camera on the current geometry
|
260 | * @param {THREE.Object3D} [obj] Object to focus on
|
261 | */
|
262 | FluxViewport.prototype.focus = function(obj) {
|
263 | this._renderer.focus(obj);
|
264 | };
|
265 |
|
266 | /**
|
267 | * Restore the camera to a default location
|
268 | */
|
269 | FluxViewport.prototype.homeCamera = function() {
|
270 | this._renderer.homeCamera();
|
271 | };
|
272 |
|
273 | /**
|
274 | * Whether to draw helpers (axis and grid)
|
275 | *
|
276 | * @param {Boolean} visible False to hide them
|
277 | */
|
278 | FluxViewport.prototype.setHelpersVisible = function(visible) {
|
279 | this._renderer.setHelpersVisible(visible);
|
280 | };
|
281 |
|
282 | /**
|
283 | * Set the viewport geomtery from a JSON string
|
284 | * @param {String} dataString The geometry to render formatted as JSON containing Flux entities
|
285 | * @return {Object} Promise to resolve after geometry is created
|
286 | */
|
287 | FluxViewport.prototype.setGeometryJson = function(dataString) {
|
288 | var dataObj = JSON.parse(dataString);
|
289 | return this.setGeometryEntity(dataObj);
|
290 | };
|
291 |
|
292 | /**
|
293 | * Set the viewport geometry from a data object containing Flux entities.
|
294 | * See documentation for more explanation of <a href="https://community.flux.io/content/kbentry/2579/geometric-primitives.html">entities</a>
|
295 | * and <a href="https://community.flux.io/content/kbentry/3087/what-is-a-scene.html">scene primitives</a>
|
296 | * @param {Object} data The geometry entities to render
|
297 | * @return {Object} Promise to resolve after geometry is created
|
298 | */
|
299 | FluxViewport.prototype.setGeometryEntity = function(data) {
|
300 | var _this = this;
|
301 | // The flow sends the same value twice, so we assume that requests
|
302 | // sent while there is already one pending are redundant
|
303 | // TODO(Kyle): This is a hack that we can remove once there are not always duplicate requests
|
304 | return new Promise(function (resolve, reject) {
|
305 | if (!_this.running) {
|
306 | _this.running = true;
|
307 | return _this._sceneBuilder.convert(data).then(function (results) {
|
308 | var object = results.getObject();
|
309 | _this._entities = object ? data : null;
|
310 | _this._updateModel(object);
|
311 | _this._latestSceneResults = results;
|
312 | _this.running = false;
|
313 | resolve(results);
|
314 | }).catch(function (err) {
|
315 | console.warn(err); // eslint-disable-line no-console
|
316 | _this.running = false;
|
317 | });
|
318 | } else {
|
319 | reject(new Error('Already running. You can only convert one entity at a time.'));
|
320 | }
|
321 | }).catch(function (err) {
|
322 | if (err.message.indexOf('running') === -1) {
|
323 | console.log(err); // eslint-disable-line no-console
|
324 | }
|
325 | throw err;
|
326 | });
|
327 | };
|
328 |
|
329 | /**
|
330 | * Change the geometry being rendered
|
331 | * @private
|
332 | * @param {THREE.Object3D} newModel The new model to render
|
333 | * @param {THREE.Object3D} oldModel The old model to remove
|
334 | */
|
335 | FluxViewport.prototype._updateModel = function(newModel) {
|
336 | this._renderer.setModel(newModel);
|
337 | if (this._autoFocus) {
|
338 | this.focus(); // changing the controls will trigger a render
|
339 | this._autoFocus = false;
|
340 | } else {
|
341 | this.render();
|
342 | }
|
343 | };
|
344 |
|
345 | /**
|
346 | * Make serializable by pruning all references and building an object property tree
|
347 | * @return {Object} Data to stringify
|
348 | */
|
349 | FluxViewport.prototype.toJSON = function() {
|
350 | var serializableState = {
|
351 | entities: this._entities,
|
352 | renderer: this._renderer.toJSON(),
|
353 | autoFocus: this._autoFocus
|
354 | };
|
355 | return serializableState;
|
356 | };
|
357 |
|
358 | /**
|
359 | * Take a data object and use it to update the viewport's internal state<br>
|
360 | * Warning this is async when it sets entities
|
361 | * @param {Object} state The properties to set
|
362 | * @return {Promise} Completion promise
|
363 | */
|
364 | FluxViewport.prototype.fromJSON = function(state) {
|
365 | if (!state) return Promise.resolve();
|
366 | var _this = this;
|
367 | if (state.entities) {
|
368 | return this.setGeometryEntity(state.entities).then(function () {
|
369 | _this._fromJSONHelper(state);
|
370 | });
|
371 | } else {
|
372 | this._fromJSONHelper(state);
|
373 | return Promise.resolve();
|
374 | }
|
375 | };
|
376 |
|
377 | /**
|
378 | * Rehydrate everything but the entities.
|
379 | * @private
|
380 | * @param {Object} state Parameter data
|
381 | */
|
382 | FluxViewport.prototype._fromJSONHelper = function(state) {
|
383 | if (state.renderer != null) {
|
384 | this._renderer.fromJSON(state.renderer);
|
385 | }
|
386 | if (state.autoFocus != null) {
|
387 | this._autoFocus = state.autoFocus;
|
388 | }
|
389 | };
|
390 |
|
391 | /**
|
392 | * Download all the geometry settings and raster image that are the state of this viewport.
|
393 | * Used for QA testing.
|
394 | * @param {String} prefix File name prefix for download path
|
395 | */
|
396 | FluxViewport.prototype.downloadState = function(prefix) {
|
397 | this._downloadJson(this.toJSON(), prefix);
|
398 | this._download(this._renderer.getGlCanvas().toDataURL('image/png'), prefix+'.png');
|
399 | };
|
400 |
|
401 | /**
|
402 | * Helper function to download some data from a url
|
403 | * @private
|
404 | * @param {DOMString} dataUrl The url containing the data to download
|
405 | * @param {String} filename The name of the file when it downloads
|
406 | */
|
407 | FluxViewport.prototype._download = function(dataUrl, filename) {
|
408 | var a = document.createElement('a');
|
409 | a.href = dataUrl;
|
410 | a.download = filename;
|
411 | a.click();
|
412 | };
|
413 |
|
414 | /**
|
415 | * Create a link and a temporary blob url to use to download from.
|
416 | * @private
|
417 | * @param {Object} data The serializable data to write as JSON
|
418 | * @param {String} prefix The file name prefix
|
419 | */
|
420 | FluxViewport.prototype._downloadJson = function(data, prefix) {
|
421 | if (this._downloadUrl) {
|
422 | window.URL.revokeObjectURL(this._downloadUrl);
|
423 | }
|
424 | var jsonString = JSON.stringify(data, null, 2);
|
425 | this._downloadUrl = window.URL.createObjectURL(new Blob([jsonString]), {type: 'text/json'});
|
426 | this._download(this._downloadUrl, prefix+'.json');
|
427 | };
|
428 |
|
429 | /**
|
430 | * Create a default 3 light rig on the renderer's scene.
|
431 | */
|
432 | FluxViewport.prototype.setupDefaultLighting = function() {
|
433 | var lighting = new THREE.Object3D();
|
434 | lighting.name = 'Lights';
|
435 |
|
436 | //TODO(Kyle) non static lighting
|
437 | this._keyLight = new THREE.DirectionalLight();
|
438 | this._keyLight.position.set(60, 80, 50);
|
439 | this._keyLight.intensity = 2.95;
|
440 | lighting.add(this._keyLight);
|
441 |
|
442 | var backLight = new THREE.DirectionalLight();
|
443 | backLight.position.set(-250, 50, -200);
|
444 | backLight.intensity = 1.7;
|
445 | lighting.add(backLight);
|
446 |
|
447 | var fillLight = new THREE.DirectionalLight();
|
448 | fillLight.position.set(-500, -500, 0);
|
449 | fillLight.intensity = 0.9;
|
450 | lighting.add(fillLight);
|
451 |
|
452 | this._renderer.setLights(lighting);
|
453 | };
|
454 |
|
455 | //---- Pass through functions
|
456 |
|
457 | /**
|
458 | * Set the size of the render canvas
|
459 | * @param {Number} width Pixels
|
460 | * @param {Number} height Pixels
|
461 | */
|
462 | FluxViewport.prototype.setSize = function(width, height) {
|
463 | this._renderer.setSize(width, height);
|
464 | };
|
465 |
|
466 | /**
|
467 | * Set the background color of the render canvas
|
468 | * @param {THREE.color} color Background color
|
469 | * @param {Number} alpha Opacity
|
470 | */
|
471 | FluxViewport.prototype.setClearColor = function(color, alpha) {
|
472 | this._renderer.setClearColor(color, alpha);
|
473 | };
|
474 |
|
475 | /**
|
476 | * Set which camera view to use (ex perspective, top etc.).
|
477 | * @param {FluxCameras.VIEWS} view The new view mode
|
478 | */
|
479 | FluxViewport.prototype.setView = function(view) {
|
480 | this._renderer.setView(view);
|
481 | this.focus();
|
482 | };
|
483 |
|
484 | /**
|
485 | * Return the views enumeration.
|
486 | * Allows you to change the view to perspective, top, etc.
|
487 | * @return {Object} Enumeration of view options for cameras
|
488 | */
|
489 | FluxViewport.getViews = function() {
|
490 | return FluxCameras.VIEWS;
|
491 | };
|
492 |
|
493 | /**
|
494 | * Set the density of the exponential fog. Generally on the order of 0.0001
|
495 | * @param {Number} density How much fog
|
496 | */
|
497 | FluxViewport.prototype.setFogDensity = function(density) {
|
498 | this._renderer._fog.density = density;
|
499 | };
|
500 |
|
501 | /**
|
502 | * Set the url of the tessellation service.
|
503 | * This can be used to replace the value if you did not set it on the constructor.
|
504 | * @param {String} newUrl The url of the tessellation server
|
505 | */
|
506 | FluxViewport.prototype.setTessUrl = function(newUrl) {
|
507 | this._sceneBuilder.setTessUrl(newUrl);
|
508 | };
|
509 |
|
510 | /**
|
511 | * Set whether the viewport should focus the geometry when it is changed
|
512 | * @param {Boolean} focus Whether to auto focus
|
513 | */
|
514 | FluxViewport.prototype.setAutoFocus = function(focus) {
|
515 | this._autoFocus = focus;
|
516 | };
|
517 |
|
518 | /**
|
519 | * Get whether the viewport will focus the geometry when it is changed
|
520 | * @return {Boolean} Whether to auto focus
|
521 | */
|
522 | FluxViewport.prototype.getAutoFocus = function() {
|
523 | return this._autoFocus;
|
524 | };
|
525 |
|
526 | /**
|
527 | * Set the edges rendering mode for hidden line rendering
|
528 | * @param {FluxViewport.EDGES_MODES} mode Whether to render front, back, both or none
|
529 | */
|
530 | FluxViewport.prototype.setEdgesMode = function(mode) {
|
531 | this._renderer.setEdgesMode(mode);
|
532 | };
|
533 |
|
534 | /**
|
535 | * Get the canvas for use in QA scripts
|
536 | * @return {Canvas} WebGL canvas dom element
|
537 | */
|
538 | FluxViewport.prototype.getGlCanvas = function() {
|
539 | return this._renderer.getGlCanvas();
|
540 | };
|
541 |
|
542 | /**
|
543 | * Turn on shadow rendering (not implemented)
|
544 | */
|
545 | FluxViewport.prototype.activateShadows = function() {
|
546 | print.warn('Shadows are not implemented yet');
|
547 | // https://vannevar.atlassian.net/browse/LIB3D-97
|
548 | };
|