diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00a178e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_STORE +.idea +*.obj +*.ply +*.mp4 +part_hier_templates +partnet_videos +dump.sql diff --git a/README.md b/README.md new file mode 100644 index 0000000..4551fab --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# PartNet: A Large-scale Benchmark for Fine-grained and Hierarchical Part-level 3D Object Understanding + +![Annotation System Overview](https://github.com/daerduoCarey/partnet_anno_system/blob/master/images/gui.png) + +**Figure 1. The PartNet Annotation System Overview.** + +## Annotation System + +This repo contains the web-based part segmentation annotation interface for PartNet. + +Our 3D web-based GUI is build upon Node.js, Express.js and Three.js frameworks. Please check the README in `client` and `server` folders for setup instructions. + + +## Paper and Dataset + +PartNet is accepted to CVPR 2019. See you at Long Beach, LA. + +Our team: [Kaichun Mo](https://cs.stanford.edu/~kaichun), [Shilin Zhu](http://cseweb.ucsd.edu/~shz338/), [Angel X. Chang](https://angelxuanchang.github.io/), [Li Yi](https://cs.stanford.edu/~ericyi/), [Subarna Tripathi](https://subarnatripathi.github.io/), [Leonidas J. Guibas](https://geometry.stanford.edu/member/guibas/) and [Hao Su](http://cseweb.ucsd.edu/~haosu/) from Stanford, UCSD, SFU and Intel AI Lab. + +Arxiv Version: https://arxiv.org/abs/1812.02713 + +Project Page: https://cs.stanford.edu/~kaichun/partnet/ + +Video: https://youtu.be/7pEuoxmb-MI + +Please refer to [this repo](https://github.com/daerduocarey/partnet_dataset) for the PartNet dataset utilities and [this repo](https://github.com/daerduocarey/partnet_seg_exps) for the segmentation experiments (Section 5) in the paper. + +## Citations + + @article{mo2018partnet, + title={{PartNet}: A Large-scale Benchmark for Fine-grained and Hierarchical Part-level {3D} Object Understanding}, + author={Mo, Kaichun and Zhu, Shilin and Chang, Angel and Yi, Li and Tripathi, Subarna and Guibas, Leonidas and Su, Hao}, + booktitle={Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR)}, + year={2019} + } + +## License + +MIT Licence + +## Updates + +* [April 18, 2019] PartNet Annotation System v1.0 release. + diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..dd87e2d --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,2 @@ +node_modules +build diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..58aef54 --- /dev/null +++ b/client/README.md @@ -0,0 +1,57 @@ +## Client-side Code + +### Installation + +The client side use `node.js` and `browserify`. + +First, install `node.js` and `browserify` as follows. + + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash + source ~/.bashrc + nvm install node + npm install -g browserify + +The part hier visualization uses [Bootstrap Tree View](https://github.com/jonmiles/bootstrap-treeview). Please install + + npm install -g bower + npm install bootstrap-treeview + +Next, install `node.js` dependent modules as defined in `package.json`. + + npm install + +Finally, compile the client-side javascript dependencies using `browserify` by running the following. + + ./build.sh + +This will generate a `build/ShapeNetPP.bundle.js` that includes all node-js modules defined by `require`. + + +### Usage + +Set up config files, + + cp config/backend.js.template config/backend.js + [fill in the information in this file] + +To run the system, please go to server-side code, set up server-side and run + + cd ../server + # read README for the server side to install + npm start + +Open browser, and go to + + http://localhost:3000 + + +### Interface Descriptions + +There are three main webpages that construct the system: login page, viewer page, and annotator page. + +* The login page provides you functions to register yourself as a worker and log in to annotate. +* The viewer page lists all annotations that have been done and provides functions to label new shapes. Each worker can see the models annotated by himself or herself only. We reserve a special username *admin* that can manage all annotations from all workers. Admin account also has the privilege to download the annotations. +* The annotator page is the main page where annotation for a model happens. It provides many utilities to view the 3D model, view part templates, annotate a part in the template, click-and-group parts, and split parts by cutting. Please click *help* on the top-left corner on the webpage for keyboard shortcuts. + +We strongly recommend you to refer to [the paper](https://arxiv.org/abs/1812.02713) and [the Youtube annotation video](https://youtu.be/7pEuoxmb-MI) for mode detailed demonstrations. + diff --git a/client/admin_list_viewer.js b/client/admin_list_viewer.js new file mode 100644 index 0000000..22fd50c --- /dev/null +++ b/client/admin_list_viewer.js @@ -0,0 +1,121 @@ +window.load_cat_names = function() { + console.log('[load_cat_names] start!'); + + var keys = ['Bag', 'Bed', 'Bottle', 'Bowl', 'Chair', 'Clock', 'Dishwasher', 'Display', 'Door', 'Earphone', + 'Faucet', 'Hat', 'Keyboard', 'Knife', 'Lamp', 'Laptop', 'Microwave', 'Mug', 'Refrigerator', 'Scissors', + 'StorageFurniture', 'Table', 'TrashCan', 'Vase']; + + keys.forEach(function(item) { + var option = document.createElement('option'); + option.value = item; + option.text = item; + $('#select_cat_name').append(option); + }); +}; + +window.post = function(path, params, method) { + method = method || "post"; // Set method to post by default if not specified. + + // The rest of this code assumes you are not using a library. + // It can be made less wordy if you use one. + var form = document.createElement("form"); + form.setAttribute("method", method); + form.setAttribute("action", path); + form.setAttribute("target", "_blank"); + + for (var key in params) { + if (params.hasOwnProperty(key)) { + var hiddenField = document.createElement("input"); + hiddenField.setAttribute("type", "hidden"); + hiddenField.setAttribute("name", key); + hiddenField.setAttribute("value", params[key]); + + form.appendChild(hiddenField); + } + } + + document.body.appendChild(form); + form.submit(); +}; + +window.gen_visu_list = function() { + var table = document.getElementById("myTable").getElementsByTagName('tbody')[0]; + var cat_name = document.getElementById('select_cat_name').value; + + if ($.fn.DataTable.isDataTable("#myTable")) { + $('#myTable').DataTable().clear().destroy(); + } + + table.innerHTML = ''; + + $.getJSON('annotation/get_all_annotations_admin/'+cat_name, function(data) { + for (let item of data.anno_list) { + var anno_id = item.anno_id; + var model_id = item.model_id; + var anno_version = item.version; + var user_id = item.user_id; + + var row = table.insertRow(-1); + + row.insertCell(0).innerHTML = anno_id; + row.insertCell(1).innerHTML = anno_version; + + row.insertCell(2).innerHTML = model_id; + + var img_html = ''; + row.insertCell(3).innerHTML = img_html; + + var view_html = ''; + row.insertCell(4).innerHTML = view_html; + + var modify_html = ''; + row.insertCell(5).innerHTML = modify_html; + + var delete_html = ''; + row.insertCell(6).innerHTML = delete_html; + + var download_html = ''; + row.insertCell(7).innerHTML = download_html; + } + + $('#myTable').DataTable({ + createdRow: function( row, data, dataIndex ) { + if ( data[2] == "0" ) { + $(row).addClass('highlightRow'); + } + } + }); + }); +}; + +window.view_model = function(anno_id) { + post('/part_annotator', {anno_id: anno_id, allow_edit: false, load_parent_anno: true}); +}; + +window.modify_model = function(anno_id) { + post('/part_annotator', {anno_id: anno_id, allow_edit: true, load_parent_anno: true}); +}; + +window.delete_model = function(anno_id) { + var prompt_str, alert_str; + prompt_str = 'Are you sure to delete this annotation? All related information will be removed.'; + alert_str = 'Deleted! The webpage will refresh automatically.'; + if (confirm(prompt_str)) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function () { + if (this.readyState === 4 && this.status === 200) { + console.log('anno deleted: ' + anno_id); + alert(alert_str); + gen_visu_list(); + } + }; + xmlhttp.open("GET", 'annotation/delete/' + anno_id, true); + xmlhttp.send(); + } +}; + +window.view_template = function() { + console.log('View template'); + var cat_name = document.getElementById('select_cat_name').value; + window.open('/template_viewer/'+cat_name); +}; diff --git a/client/build.sh b/client/build.sh new file mode 100755 index 0000000..36d2ae0 --- /dev/null +++ b/client/build.sh @@ -0,0 +1 @@ +rm -rf build && mkdir build && browserify admin_list_viewer.js template_viewer.js part_hier_annotator.js > build/ShapeNetPP.bundle.js diff --git a/client/config/.gitignore b/client/config/.gitignore new file mode 100644 index 0000000..62a0949 --- /dev/null +++ b/client/config/.gitignore @@ -0,0 +1 @@ +backend.js diff --git a/client/config/backend.js.template b/client/config/backend.js.template new file mode 100644 index 0000000..d02aa7d --- /dev/null +++ b/client/config/backend.js.template @@ -0,0 +1,32 @@ +var be_config = exports; + +// Host and Port Information (SET UP HERE!) +be_config.remoteHost = '[TODO: e.g. http://localhost]'; +be_config.remotePort = '[TODO]'; + +// get annotation info (Don't Change!) +be_config.get_anno_info = '/annotation/get_info'; +be_config.save_anno_json = '/annotation/save_json'; +be_config.get_anno_json = '/annotation/get_json'; +be_config.save_anno_snapshot = '/annotation/save_snapshot'; +be_config.update_anno_version = '/annotation/update_version'; +be_config.get_anno_obj_list = '/annotation/get_obj_list'; +be_config.save_anno_obj_list = '/annotation/save_obj_list'; +be_config.get_qa_data = '/annotation/get_qa'; +be_config.save_qa_data = '/annotation/save_qa'; + +// get files (Don't Change!) +be_config.get_original_part = '/file/original-part'; +be_config.get_remesh_part = '/file/remesh-part'; +be_config.get_new_part = '/file/new-part'; +be_config.get_original_scene_graph = '/file/original-scene-graph'; +be_config.get_remesh_cut_json = '/file/remesh-cut-output-json'; +be_config.get_model_screenshot = '/file/model-sceneshot'; + +// remesh related (Don't Change!) +be_config.request_remesh = '/remesh'; +be_config.submit_remesh = '/submit_remesh_cut'; + +// user anno list viewer (Don't Change!) +be_config.anno_list_viewer = '/part_anno_list_viewer'; + diff --git a/client/config/colors.js b/client/config/colors.js new file mode 100644 index 0000000..ed9dc71 --- /dev/null +++ b/client/config/colors.js @@ -0,0 +1,10 @@ +var colors = exports; +colors.colors = [ 0xff0000, 0x00ffff, 0x00ff00, + 0x0000ff, 0xffff00, 0xff00ff, + 0x800080, 0xadd8e6, 0x0000a0, + 0xa52a2a, 0x0000a0, 0x808000, + 0xd1c69e, 0x670000, 0x4d0000, + 0xdcf5e9, 0xaa93e2, 0x008080, + 0xf9d3d3, 0xffc0cb, 0xd95e40, + 0xb59265 +]; \ No newline at end of file diff --git a/client/css/Treant.css b/client/css/Treant.css new file mode 100644 index 0000000..3ef3675 --- /dev/null +++ b/client/css/Treant.css @@ -0,0 +1,11 @@ +/* required LIB STYLES */ +/* .Treant se automatski dodaje na svaki chart conatiner */ +.Treant { position: relative; overflow: hidden; padding: 0 !important; } +.Treant > .node, +.Treant > .pseudo { position: absolute; display: block; visibility: hidden; } +.Treant.Treant-loaded .node, +.Treant.Treant-loaded .pseudo { visibility: visible; } +.Treant > .pseudo { width: 0; height: 0; border: none; padding: 0; } +.Treant .collapse-switch { width: 3px; height: 3px; display: block; border: 1px solid black; position: absolute; top: 1px; right: 1px; cursor: pointer; } +.Treant .collapsed .collapse-switch { background-color: #868DEE; } +.Treant > .node img { border: none; float: left; } \ No newline at end of file diff --git a/client/css/custom-color-plus-scrollbar.css b/client/css/custom-color-plus-scrollbar.css new file mode 100644 index 0000000..b5596de --- /dev/null +++ b/client/css/custom-color-plus-scrollbar.css @@ -0,0 +1,44 @@ +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { margin:0; padding:0; } +table { border-collapse:collapse; border-spacing:0; } +fieldset,img { border:0; } +address,caption,cite,code,dfn,em,strong,th,var { font-style:normal; font-weight:normal; } +caption,th { text-align:left; } +h1,h2,h3,h4,h5,h6 { font-size:100%; font-weight:normal; } +q:before,q:after { content:''; } +abbr,acronym { border:0; } + +body { background: #fff; } +/* optional Container STYLES */ +.chart { height: 200px; margin: 5px; width: 1000px; margin: 5px auto; border: 3px solid #DDD; border-radius: 3px; } +.Treant > .node { } +.Treant > p { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-weight: bold; font-size: 12px; } +.node-name { font-weight: bold;} + +.nodeExample1 { + padding: 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #262626; + border: 1px solid #000; + width: 200px; + font-family: Tahoma; + font-size: 12px; + color: #ffffff; +} + +.nodeExample1 img { + margin-right: 10px; +} + +.Treant > a:link { + color: #ffffff; +} + +.Treant > a:visited { + color: #ffffff; +} + +.Treant > a:link:hover { + color: #636363; +} \ No newline at end of file diff --git a/client/css/font-awesome.min.css b/client/css/font-awesome.min.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/client/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/client/css/perfect-scrollbar.css b/client/css/perfect-scrollbar.css new file mode 100644 index 0000000..cb75a90 --- /dev/null +++ b/client/css/perfect-scrollbar.css @@ -0,0 +1,61 @@ +.ps-container .ps-scrollbar-x { + position: absolute; /* please don't change 'position' */ + bottom: 3px; /* there must be 'bottom' for ps-scrollbar-x */ + height: 8px; + background-color: #aaa; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + opacity: 0; + filter: alpha(opacity = 0); + -webkit-transition: opacity.2s linear; + -moz-transition: opacity .2s linear; + transition: opacity .2s linear; +} + +.ps-container:hover .ps-scrollbar-x { + opacity: 0.6; + filter: alpha(opacity = 60); +} + +.ps-container .ps-scrollbar-x:hover { + opacity: 0.9; + filter: alpha(opacity = 90); + cursor:default; +} + +.ps-container .ps-scrollbar-x.in-scrolling { + opacity: 0.9; + filter: alpha(opacity = 90); +} + +.ps-container .ps-scrollbar-y { + position: absolute; /* please don't change 'position' */ + right: 3px; /* there must be 'right' for ps-scrollbar-y */ + width: 8px; + background-color: #aaa; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + opacity: 0; + filter: alpha(opacity = 0); + -webkit-transition: opacity.2s linear; + -moz-transition: opacity .2s linear; + transition: opacity .2s linear; +} + +.ps-container:hover .ps-scrollbar-y { + opacity: 0.6; + filter: alpha(opacity = 60); +} + +.ps-container .ps-scrollbar-y:hover { + opacity: 0.9; + filter: alpha(opacity = 90); + cursor: default; +} + +.ps-container .ps-scrollbar-y.in-scrolling { + opacity: 0.9; + filter: alpha(opacity = 90); +} diff --git a/client/css/please-wait.css b/client/css/please-wait.css new file mode 100644 index 0000000..4399a2a --- /dev/null +++ b/client/css/please-wait.css @@ -0,0 +1,161 @@ +/* line 17, ../src/please-wait.scss */ +body.pg-loading { + overflow: hidden; +} + +/* line 21, ../src/please-wait.scss */ +.pg-loading-screen { + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: 0; + z-index: 1000000; + opacity: 1; + background-color: #FFF; + -webkit-transition: background-color 0.4s ease-in-out 0s; + -moz-transition: background-color 0.4s ease-in-out 0s; + -ms-transition: background-color 0.4s ease-in-out 0s; + -o-transition: background-color 0.4s ease-in-out 0s; + transition: background-color 0.4s ease-in-out 0s; +} +/* line 32, ../src/please-wait.scss */ +.pg-loading-screen.pg-loaded { + opacity: 0; + -webkit-animation: pgAnimLoaded 0.5s cubic-bezier(0.7, 0, 0.3, 1) both; + -moz-animation: pgAnimLoaded 0.5s cubic-bezier(0.7, 0, 0.3, 1) both; + -ms-animation: pgAnimLoaded 0.5s cubic-bezier(0.7, 0, 0.3, 1) both; + -o-animation: pgAnimLoaded 0.5s cubic-bezier(0.7, 0, 0.3, 1) both; + animation: pgAnimLoaded 0.5s cubic-bezier(0.7, 0, 0.3, 1) both; +} +/* line 38, ../src/please-wait.scss */ +.pg-loading-screen.pg-loading .pg-loading-logo-header, .pg-loading-screen.pg-loading .pg-loading-html { + opacity: 1; +} +/* line 42, ../src/please-wait.scss */ +.pg-loading-screen.pg-loading .pg-loading-logo-header, .pg-loading-screen.pg-loading .pg-loading-html:not(.pg-loaded) { + -webkit-animation: pgAnimLoading 1s cubic-bezier(0.7, 0, 0.3, 1) both; + -moz-animation: pgAnimLoading 1s cubic-bezier(0.7, 0, 0.3, 1) both; + -ms-animation: pgAnimLoading 1s cubic-bezier(0.7, 0, 0.3, 1) both; + -o-animation: pgAnimLoading 1s cubic-bezier(0.7, 0, 0.3, 1) both; + animation: pgAnimLoading 1s cubic-bezier(0.7, 0, 0.3, 1) both; +} +/* line 46, ../src/please-wait.scss */ +.pg-loading-screen.pg-loading .pg-loading-html:not(.pg-loaded) { + -webkit-animation-delay: 0.3s; + -moz-animation-delay: 0.3s; + -ms-animation-delay: 0.3s; + -o-animation-delay: 0.3s; + animation-delay: 0.3s; +} +/* line 51, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-inner { + height: 100%; + width: 100%; + margin: 0; + padding: 0; + position: static; +} +/* line 59, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-center-outer { + width: 100%; + padding: 0; + display: table !important; + height: 100%; + position: absolute; + top: 0; + left: 0; + margin: 0; +} +/* line 70, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-center-middle { + padding: 0; + vertical-align: middle; + display: table-cell !important; + margin: 0; + text-align: center; +} +/* line 78, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-logo-header, .pg-loading-screen .pg-loading-html { + width: 100%; + opacity: 0; +} +/* line 83, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-logo-header { + text-align: center; +} +/* line 86, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-logo-header img { + display: inline-block !important; +} +/* line 91, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-html { + margin-top: 90px; +} +/* line 94, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-html.pg-loaded { + -webkit-transition: opacity 0.5s cubic-bezier(0.7, 0, 0.3, 1); + -moz-transition: opacity 0.5s cubic-bezier(0.7, 0, 0.3, 1); + -ms-transition: opacity 0.5s cubic-bezier(0.7, 0, 0.3, 1); + -o-transition: opacity 0.5s cubic-bezier(0.7, 0, 0.3, 1); + transition: opacity 0.5s cubic-bezier(0.7, 0, 0.3, 1); +} +/* line 97, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-html.pg-loaded.pg-removing { + opacity: 0; +} +/* line 101, ../src/please-wait.scss */ +.pg-loading-screen .pg-loading-html.pg-loaded.pg-loading { + opacity: 1; +} + +@-webkit-keyframes pgAnimLoading { + from { + opacity: 0; + } +} +@-moz-keyframes pgAnimLoading { + from { + opacity: 0; + } +} +@-o-keyframes pgAnimLoading { + from { + opacity: 0; + } +} +@-ms-keyframes pgAnimLoading { + from { + opacity: 0; + } +} +@keyframes pgAnimLoading { + from { + opacity: 0; + } +} +@-webkit-keyframes pgAnimLoaded { + from { + opacity: 1; + } +} +@-moz-keyframes pgAnimLoaded { + from { + opacity: 1; + } +} +@-o-keyframes pgAnimLoaded { + from { + opacity: 1; + } +} +@-ms-keyframes pgAnimLoaded { + from { + opacity: 1; + } +} +@keyframes pgAnimLoaded { + from { + opacity: 1; + } +} diff --git a/client/css/star-rating.css b/client/css/star-rating.css new file mode 100644 index 0000000..95c1900 --- /dev/null +++ b/client/css/star-rating.css @@ -0,0 +1,189 @@ +/*! + * bootstrap-star-rating v4.0.3 + * http://plugins.krajee.com/star-rating + * + * Author: Kartik Visweswaran + * Copyright: 2013 - 2017, Kartik Visweswaran, Krajee.com + * + * Licensed under the BSD 3-Clause + * https://github.com/kartik-v/bootstrap-star-rating/blob/master/LICENSE.md + */ +.rating-loading { + width: 25px; + height: 25px; + font-size: 0; + color: #fff; + background: transparent url('../img/loading.gif') top left no-repeat; + border: none; +} + +/* + * Stars & Input + */ +.rating-container .rating-stars { + position: relative; + cursor: pointer; + vertical-align: middle; + display: inline-block; + overflow: hidden; + white-space: nowrap; +} + +.rating-container .rating-input { + position: absolute; + cursor: pointer; + width: 100%; + height: 1px; + bottom: 0; + left: 0; + font-size: 1px; + border: none; + background: none; + padding: 0; + margin: 0; +} + +.rating-disabled .rating-input, .rating-disabled .rating-stars { + cursor: not-allowed; +} + +.rating-container .star { + display: inline-block; + margin: 0 3px; + text-align: center; +} + +.rating-container .empty-stars { + color: #aaa; +} + +.rating-container .filled-stars { + position: absolute; + left: 0; + top: 0; + margin: auto; + color: #fde16d; + white-space: nowrap; + overflow: hidden; + -webkit-text-stroke: 1px #777; + text-shadow: 1px 1px #999; +} + +.rating-rtl { + float: right; +} + +.rating-animate .filled-stars { + transition: width 0.25s ease; + -o-transition: width 0.25s ease; + -moz-transition: width 0.25s ease; + -webkit-transition: width 0.25s ease; +} + +.rating-rtl .filled-stars { + left: auto; + right: 0; + -moz-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); + -webkit-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); + -o-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); + transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); +} + +.rating-rtl.is-star .filled-stars { + right: 0.06em; +} + +.rating-rtl.is-heart .empty-stars { + margin-right: 0.07em; +} + +/** + * Sizes + */ +.rating-xl { + font-size: 4.89em; +} + +.rating-lg { + font-size: 3.91em; +} + +.rating-md { + font-size: 3.13em; +} + +.rating-sm { + font-size: 2.5em; +} + +.rating-xs { + font-size: 2em; +} + +.rating-xl { + font-size: 4.89em; +} + +/** + * Clear + */ +.rating-container .clear-rating { + color: #aaa; + cursor: not-allowed; + display: inline-block; + vertical-align: middle; + font-size: 60%; +} + +.clear-rating-active { + cursor: pointer !important; +} + +.clear-rating-active:hover { + color: #843534; +} + +.rating-container .clear-rating { + padding-right: 5px; +} + +/** + * Caption + */ + +/* extend support to BS4 */ +.rating-container .caption .label { + display: inline-block; + padding: .25em .4em; + line-height: 1; + text-align: center; + vertical-align: baseline; + border-radius: .25rem; +} + +.rating-container .caption { + color: #999; + display: inline-block; + vertical-align: middle; + font-size: 60%; + margin-top: -0.6em; +} + +.rating-container .caption { + margin-left: 5px; + margin-right: 0; +} + +.rating-rtl .caption { + margin-right: 5px; + margin-left: 0; +} + +/** + * Print + */ +@media print { + .rating-container .clear-rating { + display: none; + } +} \ No newline at end of file diff --git a/client/css/star-rating.min.css b/client/css/star-rating.min.css new file mode 100644 index 0000000..e8f1c5b --- /dev/null +++ b/client/css/star-rating.min.css @@ -0,0 +1,10 @@ +/*! + * bootstrap-star-rating v4.0.3 + * http://plugins.krajee.com/star-rating + * + * Author: Kartik Visweswaran + * Copyright: 2013 - 2017, Kartik Visweswaran, Krajee.com + * + * Licensed under the BSD 3-Clause + * https://github.com/kartik-v/bootstrap-star-rating/blob/master/LICENSE.md + */.rating-loading{width:25px;height:25px;font-size:0;color:#fff;background:url(../img/loading.gif) top left no-repeat;border:none}.rating-container .rating-stars{position:relative;cursor:pointer;vertical-align:middle;display:inline-block;overflow:hidden;white-space:nowrap}.rating-container .rating-input{position:absolute;cursor:pointer;width:100%;height:1px;bottom:0;left:0;font-size:1px;border:none;background:0 0;padding:0;margin:0}.rating-disabled .rating-input,.rating-disabled .rating-stars{cursor:not-allowed}.rating-container .star{display:inline-block;margin:0 3px;text-align:center}.rating-container .empty-stars{color:#aaa}.rating-container .filled-stars{position:absolute;left:0;top:0;margin:auto;color:#fde16d;white-space:nowrap;overflow:hidden;-webkit-text-stroke:1px #777;text-shadow:1px 1px #999}.rating-rtl{float:right}.rating-animate .filled-stars{transition:width .25s ease;-o-transition:width .25s ease;-moz-transition:width .25s ease;-webkit-transition:width .25s ease}.rating-rtl .filled-stars{left:auto;right:0;-moz-transform:matrix(-1,0,0,1,0,0) translate3d(0,0,0);-webkit-transform:matrix(-1,0,0,1,0,0) translate3d(0,0,0);-o-transform:matrix(-1,0,0,1,0,0) translate3d(0,0,0);transform:matrix(-1,0,0,1,0,0) translate3d(0,0,0)}.rating-rtl.is-star .filled-stars{right:.06em}.rating-rtl.is-heart .empty-stars{margin-right:.07em}.rating-lg{font-size:3.91em}.rating-md{font-size:3.13em}.rating-sm{font-size:2.5em}.rating-xs{font-size:2em}.rating-xl{font-size:4.89em}.rating-container .clear-rating{color:#aaa;cursor:not-allowed;display:inline-block;vertical-align:middle;font-size:60%;padding-right:5px}.clear-rating-active{cursor:pointer!important}.clear-rating-active:hover{color:#843534}.rating-container .caption .label{display:inline-block;padding:.25em .4em;line-height:1;text-align:center;vertical-align:baseline;border-radius:.25rem}.rating-container .caption{color:#999;display:inline-block;vertical-align:middle;font-size:60%;margin-top:-.6em;margin-left:5px;margin-right:0}.rating-rtl .caption{margin-right:5px;margin-left:0}@media print{.rating-container .clear-rating{display:none}} \ No newline at end of file diff --git a/client/css/template_viewer.css b/client/css/template_viewer.css new file mode 100644 index 0000000..214ed87 --- /dev/null +++ b/client/css/template_viewer.css @@ -0,0 +1,69 @@ +@import url(https://fonts.googleapis.com/css?family=Work+Sans:300,600); + +html,body{ + font-size: 20px; + font-family: 'Work Sans', sans-serif; + height:100%; + width:100%; + display: block; + overflow: hidden; +} +h3{ + margin: 0px; + font-size: 20px; + color: red; +} + +.main_row { + width: 100%; + height:100%; +} + +.main_hier { + float: left; + width: 30%; + border-style: solid; + height: 100%; + overflow: scroll; +} + +.main_qa { + float: left; + width: 70%; + border-style: solid; + height: 100%; + overflow: scroll; +} + +#part_definition { + display: block; position: relative; +} + +#tree_instance { + clear: both; + float: bottom; + height:80%; + display: block; position: relative; +} + +/* optional Container STYLES */ +.chart { height: 600px; margin: 5px; width: 900px; } +.Treant > .node { } +.Treant > p { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-weight: bold; font-size: 12px; } +.node-name { font-weight: bold;} + +.nodeExample1 { + padding: 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #ffffff; + border: 1px solid #000; + width: 200px; + font-family: Tahoma; + font-size: 12px; +} + +.nodeExample1 img { + margin-right: 10px; +} diff --git a/client/css/ui.css b/client/css/ui.css new file mode 100644 index 0000000..20dd761 --- /dev/null +++ b/client/css/ui.css @@ -0,0 +1,122 @@ +@import url(https://fonts.googleapis.com/css?family=Work+Sans:300,600); + +html,body{ + font-size: 20px; + font-family: 'Work Sans', sans-serif; + height:100%; + width:100%; + display: block; + overflow: hidden; +} +h3{ + margin: 0px; + font-size: 20px; + color: red; +} + +.main_row { + width: 100%; + height:100%; +} + +.main_hier { + float: left; + width: 30%; + border-style: solid; + height: 100%; + overflow: scroll; +} + +.main_qa { + float: left; + width: 30%; + border-style: solid; + height: 100%; + overflow: scroll; +} + +.main_3d { + float: left; + width: 40%; + border-style: solid; + height: 100%; + overflow:hidden; +} + +textarea +{ + width:100%; + height:100px; +} + +#main_3d_canvas { + width: 100%; + height: 70vh; + border-bottom: 3px solid #000; +} + +#model_screenshots { + width: 100%; + padding-top: 40px; + height: 30vh; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; +} + +.txt_alert { + color: #ff0000; +} + +.txt_define { + color: #0000ff; +} + +#button_nav { + display: block; + position: relative; +} + +#annotation_quiz { + clear: both; + opacity: 1; + transition: opacity 0.5s; + display: block; + position: relative; + border-bottom:3px solid #000; +} + +#part_definition { + display: block; position: relative; +} + +#tree_instance { + clear: both; + float: bottom; + height:80%; + display: block; position: relative; +} + +.question{ + font-size: 20px; + margin-bottom: 10px; +} +.answers { + margin-bottom: 20px; + text-align: left; + display: inline-block; +} +.answers label{ + float: none; + display: inline-block; + vertical-align: middle; +} + +input[type='number'] { + -moz-appearance:textfield; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; +} diff --git a/client/js/OBJLoader.js b/client/js/OBJLoader.js new file mode 100644 index 0000000..609d151 --- /dev/null +++ b/client/js/OBJLoader.js @@ -0,0 +1,720 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +THREE.OBJLoader = ( function () { + + // o object_name | g group_name + var object_pattern = /^[og]\s*(.+)?/; + // mtllib file_reference + var material_library_pattern = /^mtllib /; + // usemtl material_name + var material_use_pattern = /^usemtl /; + + function ParserState() { + + var state = { + objects: [], + object: {}, + + vertices: [], + normals: [], + colors: [], + uvs: [], + + materialLibraries: [], + + startObject: function ( name, fromDeclaration ) { + + // If the current object (initial from reset) is not from a g/o declaration in the parsed + // file. We need to use it for the first parsed g/o to keep things in sync. + if ( this.object && this.object.fromDeclaration === false ) { + + this.object.name = name; + this.object.fromDeclaration = ( fromDeclaration !== false ); + return; + + } + + var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + this.object = { + name: name || '', + fromDeclaration: ( fromDeclaration !== false ), + + geometry: { + vertices: [], + normals: [], + colors: [], + uvs: [] + }, + materials: [], + smooth: true, + + startMaterial: function ( name, libraries ) { + + var previous = this._finalize( false ); + + // New usemtl declaration overwrites an inherited material, except if faces were declared + // after the material, then it must be preserved for proper MultiMaterial continuation. + if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { + + this.materials.splice( previous.index, 1 ); + + } + + var material = { + index: this.materials.length, + name: name || '', + mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), + smooth: ( previous !== undefined ? previous.smooth : this.smooth ), + groupStart: ( previous !== undefined ? previous.groupEnd : 0 ), + groupEnd: - 1, + groupCount: - 1, + inherited: false, + + clone: function ( index ) { + + var cloned = { + index: ( typeof index === 'number' ? index : this.index ), + name: this.name, + mtllib: this.mtllib, + smooth: this.smooth, + groupStart: 0, + groupEnd: - 1, + groupCount: - 1, + inherited: false + }; + cloned.clone = this.clone.bind( cloned ); + return cloned; + + } + }; + + this.materials.push( material ); + + return material; + + }, + + currentMaterial: function () { + + if ( this.materials.length > 0 ) { + + return this.materials[ this.materials.length - 1 ]; + + } + + return undefined; + + }, + + _finalize: function ( end ) { + + var lastMultiMaterial = this.currentMaterial(); + if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) { + + lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; + lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; + lastMultiMaterial.inherited = false; + + } + + // Ignore objects tail materials if no face declarations followed them before a new o/g started. + if ( end && this.materials.length > 1 ) { + + for ( var mi = this.materials.length - 1; mi >= 0; mi -- ) { + + if ( this.materials[ mi ].groupCount <= 0 ) { + + this.materials.splice( mi, 1 ); + + } + + } + + } + + // Guarantee at least one empty material, this makes the creation later more straight forward. + if ( end && this.materials.length === 0 ) { + + this.materials.push( { + name: '', + smooth: this.smooth + } ); + + } + + return lastMultiMaterial; + + } + }; + + // Inherit previous objects material. + // Spec tells us that a declared material must be set to all objects until a new material is declared. + // If a usemtl declaration is encountered while this new object is being parsed, it will + // overwrite the inherited material. Exception being that there was already face declarations + // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + + if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) { + + var declared = previousMaterial.clone( 0 ); + declared.inherited = true; + this.object.materials.push( declared ); + + } + + this.objects.push( this.object ); + + }, + + finalize: function () { + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + }, + + parseVertexIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseNormalIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseUVIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; + + }, + + addVertex: function ( a, b, c ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addVertexLine: function ( a ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + + }, + + addNormal: function ( a, b, c ) { + + var src = this.normals; + var dst = this.object.geometry.normals; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addColor: function ( a, b, c ) { + + var src = this.colors; + var dst = this.object.geometry.colors; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addUV: function ( a, b, c ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ], src[ a + 1 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ] ); + + }, + + addUVLine: function ( a ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ], src[ a + 1 ] ); + + }, + + addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) { + + var vLen = this.vertices.length; + + var ia = this.parseVertexIndex( a, vLen ); + var ib = this.parseVertexIndex( b, vLen ); + var ic = this.parseVertexIndex( c, vLen ); + + this.addVertex( ia, ib, ic ); + + if ( ua !== undefined ) { + + var uvLen = this.uvs.length; + + ia = this.parseUVIndex( ua, uvLen ); + ib = this.parseUVIndex( ub, uvLen ); + ic = this.parseUVIndex( uc, uvLen ); + + this.addUV( ia, ib, ic ); + + } + + if ( na !== undefined ) { + + // Normals are many times the same. If so, skip function call and parseInt. + var nLen = this.normals.length; + ia = this.parseNormalIndex( na, nLen ); + + ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); + ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); + + this.addNormal( ia, ib, ic ); + + } + + if ( this.colors.length > 0 ) { + + this.addColor( ia, ib, ic ); + + } + + }, + + addLineGeometry: function ( vertices, uvs ) { + + this.object.geometry.type = 'Line'; + + var vLen = this.vertices.length; + var uvLen = this.uvs.length; + + for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { + + this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); + + } + + for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { + + this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); + + } + + } + + }; + + state.startObject( '', false ); + + return state; + + } + + // + + function OBJLoader( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + + this.materials = null; + + } + + OBJLoader.prototype = { + + constructor: OBJLoader, + + load: function ( url, onLoad, onProgress, onError ) { + + var scope = this; + + var loader = new THREE.FileLoader( scope.manager ); + loader.setPath( this.path ); + loader.load( url, function ( text ) { + onLoad( scope.parse( text ) ); + + }, onProgress, onError ); + + }, + + load_from_server: function(url, onLoad, onProgress, onError){ + var scope = this; + + onLoad(scope.parse(url)); + }, + + setPath: function ( value ) { + + this.path = value; + + }, + + setMaterials: function ( materials ) { + + this.materials = materials; + + return this; + + }, + + parse: function ( text ) { + + console.time( 'OBJLoader' ); + + var state = new ParserState(); + + if ( text.indexOf( '\r\n' ) !== - 1 ) { + + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); + + } + + if ( text.indexOf( '\\\n' ) !== - 1 ) { + + // join lines separated by a line continuation character (\) + text = text.replace( /\\\n/g, '' ); + + } + + var lines = text.split( '\n' ); + var line = '', lineFirstChar = ''; + var lineLength = 0; + var result = []; + + // Faster to just trim left side of the line. Use if available. + var trimLeft = ( typeof ''.trimLeft === 'function' ); + + for ( var i = 0, l = lines.length; i < l; i ++ ) { + + line = lines[ i ]; + + line = trimLeft ? line.trimLeft() : line.trim(); + + lineLength = line.length; + + if ( lineLength === 0 ) continue; + + lineFirstChar = line.charAt( 0 ); + + // @todo invoke passed in handler if any + if ( lineFirstChar === '#' ) continue; + + if ( lineFirstChar === 'v' ) { + + var data = line.split( /\s+/ ); + + switch ( data[ 0 ] ) { + + case 'v': + state.vertices.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ), + parseFloat( data[ 3 ] ) + ); + if ( data.length === 8 ) { + + state.colors.push( + parseFloat( data[ 4 ] ), + parseFloat( data[ 5 ] ), + parseFloat( data[ 6 ] ) + + ); + + } + break; + case 'vn': + state.normals.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ), + parseFloat( data[ 3 ] ) + ); + break; + case 'vt': + state.uvs.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ) + ); + break; + + } + + } else if ( lineFirstChar === 'f' ) { + + var lineData = line.substr( 1 ).trim(); + var vertexData = lineData.split( /\s+/ ); + var faceVertices = []; + + // Parse the face vertex data into an easy to work with format + + for ( var j = 0, jl = vertexData.length; j < jl; j ++ ) { + + var vertex = vertexData[ j ]; + + if ( vertex.length > 0 ) { + + var vertexParts = vertex.split( '/' ); + faceVertices.push( vertexParts ); + + } + + } + + // Draw an edge between the first vertex and all subsequent vertices to form an n-gon + + var v1 = faceVertices[ 0 ]; + + for ( var j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) { + + var v2 = faceVertices[ j ]; + var v3 = faceVertices[ j + 1 ]; + + state.addFace( + v1[ 0 ], v2[ 0 ], v3[ 0 ], + v1[ 1 ], v2[ 1 ], v3[ 1 ], + v1[ 2 ], v2[ 2 ], v3[ 2 ] + ); + + } + + } else if ( lineFirstChar === 'l' ) { + + var lineParts = line.substring( 1 ).trim().split( " " ); + var lineVertices = [], lineUVs = []; + + if ( line.indexOf( "/" ) === - 1 ) { + + lineVertices = lineParts; + + } else { + + for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { + + var parts = lineParts[ li ].split( "/" ); + + if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); + if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); + + } + + } + state.addLineGeometry( lineVertices, lineUVs ); + + } else if ( ( result = object_pattern.exec( line ) ) !== null ) { + + // o object_name + // or + // g group_name + + // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 + // var name = result[ 0 ].substr( 1 ).trim(); + var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); + + state.startObject( name ); + + } else if ( material_use_pattern.test( line ) ) { + + // material + + state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); + + } else if ( material_library_pattern.test( line ) ) { + + // mtl file + + state.materialLibraries.push( line.substring( 7 ).trim() ); + + } else if ( lineFirstChar === 's' ) { + + result = line.split( ' ' ); + + // smooth shading + + // @todo Handle files that have varying smooth values for a set of faces inside one geometry, + // but does not define a usemtl for each face set. + // This should be detected and a dummy material created (later MultiMaterial and geometry groups). + // This requires some care to not create extra material on each smooth value for "normal" obj files. + // where explicit usemtl defines geometry groups. + // Example asset: examples/models/obj/cerberus/Cerberus.obj + + /* + * http://paulbourke.net/dataformats/obj/ + * or + * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf + * + * From chapter "Grouping" Syntax explanation "s group_number": + * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. + * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form + * surfaces, smoothing groups are either turned on or off; there is no difference between values greater + * than 0." + */ + if ( result.length > 1 ) { + + var value = result[ 1 ].trim().toLowerCase(); + state.object.smooth = ( value !== '0' && value !== 'off' ); + + } else { + + // ZBrush can produce "s" lines #11707 + state.object.smooth = true; + + } + var material = state.object.currentMaterial(); + if ( material ) material.smooth = state.object.smooth; + + } else { + + // Handle null terminated files without exception + if ( line === '\0' ) continue; + + throw new Error( 'THREE.OBJLoader: Unexpected line: "' + line + '"' ); + + } + + } + + state.finalize(); + + var container = new THREE.Group(); + container.materialLibraries = [].concat( state.materialLibraries ); + + for ( var i = 0, l = state.objects.length; i < l; i ++ ) { + + var object = state.objects[ i ]; + var geometry = object.geometry; + var materials = object.materials; + var isLine = ( geometry.type === 'Line' ); + + // Skip o/g line declarations that did not follow with any faces + if ( geometry.vertices.length === 0 ) continue; + + var buffergeometry = new THREE.BufferGeometry(); + + buffergeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) ); + + if ( geometry.normals.length > 0 ) { + + buffergeometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) ); + + } else { + + buffergeometry.computeVertexNormals(); + + } + + if ( geometry.colors.length > 0 ) { + + buffergeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) ); + + } + + if ( geometry.uvs.length > 0 ) { + + buffergeometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( geometry.uvs, 2 ) ); + + } + + // Create materials + + var createdMaterials = []; + + for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { + + var sourceMaterial = materials[ mi ]; + var material = undefined; + + if ( this.materials !== null ) { + + material = this.materials.create( sourceMaterial.name ); + + // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. + if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { + + var materialLine = new THREE.LineBasicMaterial(); + materialLine.copy( material ); + material = materialLine; + + } + + } + + if ( ! material ) { + + material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); + material.name = sourceMaterial.name; + + } + + material.flatShading = sourceMaterial.smooth ? false : true; + + createdMaterials.push( material ); + + } + + // Create mesh + + var mesh; + + if ( createdMaterials.length > 1 ) { + + for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { + + var sourceMaterial = materials[ mi ]; + buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); + + } + + mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials ) : new THREE.LineSegments( buffergeometry, createdMaterials ) ); + + } else { + + mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); + + } + + mesh.name = object.name; + + container.add( mesh ); + + } + + console.timeEnd( 'OBJLoader' ); + + return container; + + } + + }; + + return OBJLoader; + +} )(); diff --git a/client/js/Object3DUtil.js b/client/js/Object3DUtil.js new file mode 100644 index 0000000..355bade --- /dev/null +++ b/client/js/Object3DUtil.js @@ -0,0 +1,3250 @@ +'use strict'; + +var base = require('base'); +var BBox = require('geo/BBox'); +var Colors = require('util/Colors'); +var Constants = require('Constants'); +var GeometryUtil = require('geo/GeometryUtil'); +var Materials = require('materials/Materials'); +var RNG = require('math/RNG'); +var _ = require('util'); + +var Object3DUtil = {}; +Object3DUtil.MaterialsAll = 1; +Object3DUtil.MaterialsCompatible = 2; +Object3DUtil.MaterialsAllNonRecursive = 3; + +Object3DUtil.OutNormals = Object.freeze([ + new THREE.Vector3(-1, 0, 0), + new THREE.Vector3(+1, 0, 0), + new THREE.Vector3(0, -1, 0), + new THREE.Vector3(0, +1, 0), + new THREE.Vector3(0, 0, -1), + new THREE.Vector3(0, 0, +1) +]); +Object3DUtil.InNormals = Object.freeze([ + new THREE.Vector3(+1, 0, 0), + new THREE.Vector3(-1, 0, 0), + new THREE.Vector3(0, +1, 0), + new THREE.Vector3(0, -1, 0), + new THREE.Vector3(0, 0, +1), + new THREE.Vector3(0, 0, -1) +]); +Object3DUtil.FaceCenters01 = Object.freeze([ + new THREE.Vector3(0.0, 0.5, 0.5), + new THREE.Vector3(1.0, 0.5, 0.5), + new THREE.Vector3(0.5, 0.0, 0.5), + new THREE.Vector3(0.5, 1.0, 0.5), + new THREE.Vector3(0.5, 0.5, 0.0), + new THREE.Vector3(0.5, 0.5, 1.0) +]); +Object3DUtil.OppositeFaces = Object.freeze([1,0,3,2,5,4]); +// Rotation +Object3DUtil.PlaneToFaceRotationParams = Object.freeze([ + { axis: new THREE.Vector3(0, 1, 0), angle: Math.PI / 2 }, + { axis: new THREE.Vector3(0, 1, 0), angle: Math.PI / 2 }, + { axis: new THREE.Vector3(1, 0, 0), angle: Math.PI / 2 }, + { axis: new THREE.Vector3(1, 0, 0), angle: Math.PI / 2 }, + null, // { axis: new THREE.Vector3(0,0,1), angle: 0 }, + null //{ axis: new THREE.Vector3(0,0,1), angle: 0 } +]); + +// Set the node material, keeping multimaterial +function setNodeMaterial(node, material, materialIndex) { + if (!node.userData.origMaterial) { + node.userData.origMaterial = node.material; + } + if (node.material instanceof THREE.MultiMaterial) { + node.material = node.material.clone(); + if (materialIndex !== undefined) { + var i = materialIndex; + if (i < 0 || i >= node.material.materials.length) { + console.warn('Invalid materialIndex ' + materialIndex + ' for node'); + console.log(node); + return false; + } + node.material.materials[i] = material; + } else { + for (var i = 0; i < node.material.materials.length; i++) { + node.material.materials[i] = material; + } + } + } else { + node.material = material; + } + return true; +} + +Object3DUtil.setMaterial = function (object3D, materialOrMaterialFn, materialsToSet, saveOldMaterial, filterMeshFn) { + if (!materialsToSet) { + materialsToSet = Object3DUtil.MaterialsCompatible; + } + var skippedMeshes = 0; + var totalMeshes = 0; + var nonrecursive = materialsToSet === Object3DUtil.MaterialsAllNonRecursive; + Object3DUtil.traverseMeshes(object3D, + nonrecursive, + function (node) { + var material = (typeof materialOrMaterialFn === 'function')? + materialOrMaterialFn(node) : materialOrMaterialFn; + var applyMaterial = !!material; + if (filterMeshFn) { + applyMaterial = filterMeshFn(node, material); + } + if (applyMaterial && materialsToSet === Object3DUtil.MaterialsCompatible) { + if (material.map) { + if (!node.material.map) { + applyMaterial = false; + } + } + } + if (applyMaterial) { + if (saveOldMaterial) { + if (node.origMaterials) { + node.origMaterials.push(node.material); + } else { + node.origMaterials = [node.material]; + } + } + // node.material = material; + if (material instanceof THREE.Color) { + // copy material and just set color + var m = node.material.clone(); + m.color = material; + node.material = m; + } else { + node.material = material; + } + } else { + skippedMeshes++; + } + totalMeshes++; + }); + if (skippedMeshes) { + // console.log('Material not applied to all meshes: skipped ' + skippedMeshes + '/' + totalMeshes); + // console.log('material', materialOrMaterialFn, materialsToSet); + // console.log('node', object3D); + } + return { total: totalMeshes, skipped: skippedMeshes }; +}; + +Object3DUtil.revertMaterials = function (object3D, nonrecursive, fullRevert) { + var skippedMeshes = 0; + var totalMeshes = 0; + Object3DUtil.traverseMeshes(object3D, + nonrecursive, + function (node) { + if (node.origMaterials && node.origMaterials.length > 0) { + node.material = node.origMaterials.pop(); + if (fullRevert) { + while (node.origMaterials.length > 0) { + node.material = node.origMaterials.pop(); + } + } + } else { + skippedMeshes++; + } + totalMeshes++; + }); + return { total: totalMeshes, skipped: skippedMeshes }; +}; + +Object3DUtil.setTransparency = function (object3D, transparency) { + object3D.traverse(function (node) { + if (node instanceof THREE.Mesh) { + if (node.material) { + Object3DUtil.setMaterialOpacity(node.material, 1.0 - transparency); + } + } + }); +}; + +Object3DUtil.setDoubleSided = function (object3D) { + object3D.traverse(function (node) { + if (node.material) { + node.material.side = THREE.DoubleSide; + if (node.material.materials) { + for (var iMat = 0; iMat < node.material.materials.length; iMat++) { + node.material.materials[iMat].side = THREE.DoubleSide; + } + } + } + }); +}; + +Object3DUtil.countMirroringTransforms = function (object3D) { + var numFlips = 0; + object3D.traverseAncestors(function (node) { + var s = node.scale; + if (s.x * s.y * s.z < 0) { numFlips++; } + }); + return numFlips; +}; + +// Flip normals/face vertices for mirrored geometry +Object3DUtil.flipForMirroring = function (object3D) { + // console.time('flipNormals'); + object3D.traverse(function (node) { + if (node instanceof THREE.Mesh) { + if (node.geometry) { + var geo = node.geometry; + GeometryUtil.flipForMirroring(geo); + } + } + }); + // console.timeEnd('flipNormals'); +}; + +Object3DUtil.setMaterialOpacity = function (material, opacity) { + Materials.setMaterialOpacity(material, opacity); +}; + +Object3DUtil.setDepthWrite = function (object3D, flag) { + Object3DUtil.processMaterial(object3D, function (material) { + material.depthWrite = flag; + }); +}; + +Object3DUtil.processMaterial = function (object3D, callback) { + if (object3D instanceof THREE.Object3D) { + object3D.traverse(function (node) { + if (node instanceof THREE.Mesh) { + if (node.material) { + Object3DUtil.processMaterial(node.material, callback); + } + } + }); + } else if (object3D instanceof THREE.MultiMaterial) { + var material = object3D; + for (var i = 0; i < material.materials.length; i++) { + Object3DUtil.processMaterial(material.materials[i], callback); + } + } else if (object3D instanceof THREE.Material) { + callback(object3D); + } +}; + +Object3DUtil.createMaterial = function (params) { + return Materials.createMaterial(params); +}; + +Object3DUtil.applyMaterialMappings = function (object3D, materialMappings) { + console.log(materialMappings); + var materials = {}; + for (var id in materialMappings.materials) { + if (materialMappings.materials.hasOwnProperty(id)) { + var mat = materialMappings.materials[id]; + if (!mat.replaced) { + materials[id] = Object3DUtil.createMaterial(mat.material); + } + } + } + var meshes = {}; + for (var id in materialMappings.meshes) { + if (materialMappings.meshes.hasOwnProperty(id)) { + var mesh = materialMappings.meshes[id]; + meshes[mesh.name] = materials[mesh.materialId]; + } + } + var skippedMeshes = 0; + var totalMeshes = 0; + object3D.traverse(function (node) { + if (node instanceof THREE.Mesh) { + if (node.name) { + var material = meshes[node.name]; + if (!material) { + // hack to see if we replace the node specific name, we can get a match + var newname = node.name.replace(/.*-/, 'mesh'); + material = meshes[newname]; + } + if (material) { + node.material = material; + } else { + skippedMeshes++; + } + } + totalMeshes++; + } + }); + if (skippedMeshes) { + console.log('Material not applied to all meshes: skipped ' + skippedMeshes + '/' + totalMeshes); + } + return { total: totalMeshes, skipped: skippedMeshes }; +}; + +Object3DUtil.applyRandomMaterials = function (object3D, nonrecursive) { + var id = 0; + return Object3DUtil.applyMaterials(object3D, + function (mesh) { + return Object3DUtil.getSimpleFalseColorMaterial(id++); + }, + nonrecursive); +}; + +Object3DUtil.applyIndexedMaterials = function (object3D, nonrecursive) { + return Object3DUtil.applyMaterials(object3D, + function (mesh) { + var id = mesh.userData.index; + return Object3DUtil.getSimpleFalseColorMaterial(id); + }, + nonrecursive); +}; + +Object3DUtil.highlightMeshes = function (object3D, meshIndices, material, useIndexedMaterial) { + function getMaterial(i) { + if (material) { + return material; + } else if (useIndexedMaterial) { + return Object3DUtil.getSimpleFalseColorMaterial(i); + } else { + return Object3DUtil.getSimpleFalseColorMaterial(0); + } + } + + return Object3DUtil.applyMaterials(object3D, + function (mesh) { + if (meshIndices.length) { + for (var j = 0; j < meshIndices.length; j++) { + if (mesh.userData.index === meshIndices[j]) { + return getMaterial(mesh.userData.index); + } + } + return Object3DUtil.ClearMat; + } else if (mesh.userData.index === meshIndices) { + return getMaterial(mesh.userData.index); + } else { + return Object3DUtil.ClearMat; + } + }, + true); +}; + +Object3DUtil.applyPartMaterial = function (part, material, nonrecursive, keepMultiMaterial) { + if (part instanceof THREE.Object3D) { + Object3DUtil.applyMaterial(part, material, nonrecursive, keepMultiMaterial); + } else if (part instanceof Array) { + for (var i = 0; i < part.length; i++) { + Object3DUtil.applyPartMaterial(part[i], material, nonrecursive, keepMultiMaterial); + } + } else { + var node = part['node']; + var materialIndex = part['materialIndex']; + var materialApplied = true; + if (node) { + if (materialIndex !== undefined) { + if (node instanceof THREE.Mesh) { + materialApplied = setNodeMaterial(node, material, materialIndex); + } + } else { + Object3DUtil.applyPartMaterial(node, material, nonrecursive, keepMultiMaterial); + materialApplied = true; + } + } + if (!materialApplied) { + console.warn('Cannot apply material to part'); + console.log(part); + } + } +}; + +Object3DUtil.applyMaterial = function (object3D, material, nonrecursive, keepMultiMaterial) { + return Object3DUtil.applyMaterials(object3D, + function (mesh) { + return material; + }, + nonrecursive, keepMultiMaterial); +}; + +Object3DUtil.applyMaterials = function (object3D, getMaterialCallback, nonrecursive, keepMultiMaterial) { + var skippedMeshes = 0; + var totalMeshes = 0; + Object3DUtil.traverseMeshes(object3D, + nonrecursive, + function (node) { + var material = getMaterialCallback(node); + if (material) { + if (keepMultiMaterial) { + setNodeMaterial(node, material); + } else { + node.material = material; + } + } else { + skippedMeshes++; + } + totalMeshes++; + }); + if (skippedMeshes) { + console.log('Material not applied to all meshes: skipped ' + skippedMeshes + '/' + totalMeshes); + } + return {total: totalMeshes, skipped: skippedMeshes}; +}; + +Object3DUtil.detachFromParent = function (object3D, scene) { + // Detach from parent while keeping same world transform + if (object3D.parent) { + Object3DUtil.clearCache(object3D.parent); + object3D.parent.updateMatrixWorld(); + if (scene) { + scene.updateMatrixWorld(); + } + + var objWorldTransform = new THREE.Matrix4(); + objWorldTransform.copy(object3D.matrixWorld); + object3D.parent.remove(object3D); + + var objMinv = new THREE.Matrix4(); + objMinv.getInverse(object3D.matrix); + var matrix = new THREE.Matrix4(); + matrix.multiplyMatrices(objWorldTransform, objMinv); + + // Add to scene... + if (scene) { + var sceneMinv = new THREE.Matrix4(); + sceneMinv.getInverse(scene.matrixWorld); + matrix.multiplyMatrices(sceneMinv, matrix); + object3D.applyMatrix(matrix); + object3D.matrixWorldNeedsUpdate = true; + scene.add(object3D); + } + } +}; + +Object3DUtil.attachToParent = function (object3D, parent, scene) { + // Attach to parent while keeping same world transform + if (object3D.parent === parent) return; + if (parent) { + parent.updateMatrixWorld(); + object3D.updateMatrixWorld(); + + var objWorldTransform = new THREE.Matrix4(); + objWorldTransform.copy(object3D.matrixWorld); + var parentMinv = new THREE.Matrix4(); + parentMinv.getInverse(parent.matrixWorld); + var objMinv = new THREE.Matrix4(); + objMinv.getInverse(object3D.matrix); + + var matrix = new THREE.Matrix4(); + matrix.multiplyMatrices(parentMinv, objWorldTransform); + matrix.multiplyMatrices(matrix, objMinv); + object3D.applyMatrix(matrix); + object3D.matrixWorldNeedsUpdate = true; + + parent.add(object3D); + Object3DUtil.clearCache(parent); + } else { + Object3DUtil.detachFromParent(object3D, scene); + } +}; + +Object3DUtil.setCastShadow = function (object3D, flag) { + object3D.traverse(function (node) { + if (node.castShadow != undefined) { + //console.log('castShadow'); + node.castShadow = flag; + } + }); +}; + +Object3DUtil.setReceiveShadow = function (object3D, flag) { + object3D.traverse(function (node) { + if (node.receiveShadow != undefined) { + //console.log('receiveShadow'); + node.receiveShadow = flag; + } + }); +}; + +Object3DUtil.computeVertexMeanLocal = function (root, transform) { + root.updateMatrixWorld(); + var modelInverse = new THREE.Matrix4(); + modelInverse.getInverse(root.matrixWorld); + if (transform) { + var m = new THREE.Matrix4(); + m.multiplyMatrices(transform, modelInverse); + return Object3DUtil.computeVertexMean(root, m); + } else { + return Object3DUtil.computeVertexMean(root, modelInverse); + } +}; + +Object3DUtil.computeVertexMean = function (root, transform) { + var agg = new THREE.Vector3(); + var n = 0; + Object3DUtil.traverseMeshes(root, false, function(mesh) { + GeometryUtil.forFaceVerticesWithTransform(mesh.geometry, transform, function(v) { + agg.add(v); + n++; + }); + }); + if (n > 0) { + agg.multiplyScalar(1/n); + } + console.log('Computed mean vertex: ' + JSON.stringify(agg) + ', nvertices: ' + n); + return agg; +}; + +Object3DUtil.getBoundingBox = function (objects, force) { + if (Array.isArray(objects)) { + var bbox = new BBox(); + for (var i = 0; i < objects.length; i++) { + bbox.includeBBox(Object3DUtil.__getBoundingBox(objects[i], force)); + } + return bbox; + } else { + return Object3DUtil.__getBoundingBox(objects, force); + } +}; + +Object3DUtil.__getBoundingBox = function (root, force) { + // Have cached world bounding box + //var modelInstance = Object3DUtil.getModelInstance(root); + if (!root.cached) { + root.cached = {}; + } + if (!root.cached.worldBoundingBox || force/* || !modelInstance */) { + root.cached.worldBoundingBox = Object3DUtil.computeBoundingBox(root); + //console.log(root.cached.worldBoundingBox); + } else { + //console.log("Recompute not needed"); + } + return root.cached.worldBoundingBox; +}; + +Object3DUtil.computeBoundingBoxLocal = function (root, transform) { + root.updateMatrixWorld(); + var modelInverse = new THREE.Matrix4(); + modelInverse.getInverse(root.matrixWorld); + if (transform) { + var m = new THREE.Matrix4(); + m.multiplyMatrices(transform, modelInverse); + return Object3DUtil.computeBoundingBox(root, m); + } else { + return Object3DUtil.computeBoundingBox(root, modelInverse); + } +}; + +Object3DUtil.computeBoundingBox = function (root, transform, filter) { + var bbox = new BBox(); + //console.time("computeBoundingBox"); + //var start = Date.now(); + bbox.includeObject3D(root, transform, filter); + //var end = Date.now(); + //console.log("Get bounding box took " + (end-start) + " ms"); + //console.timeEnd("computeBoundingBox"); + return bbox; +}; + +Object3DUtil.getBoundingBoxDims = function (model, bb) { + if (!bb) bb = Object3DUtil.getBoundingBox(model); + var bbSize = new THREE.Vector3(); + bbSize.subVectors(bb.max, bb.min); + return bbSize; +}; + +Object3DUtil.getSizeByOptions = function () { + return ['height', 'length', 'width', 'max', 'volumeCubeRoot', 'diagonal']; +}; + +Object3DUtil.convertBbDimsToSize = function (bbDims, sizeBy) { + var size; + if (bbDims instanceof THREE.Vector3) { + } else if (bbDims instanceof Array) { + bbDims = new THREE.Vector3(bbDims[0], bbDims[1], bbDims[2]); + } else { + console.error('Unsupported bbDims type'); + return undefined; + } + switch (sizeBy) { + case 'height': + size = bbDims.z; + break; + case 'length': + size = bbDims.y; + break; + case 'width': + size = bbDims.x; + break; + case 'max': + size = Math.max(bbDims.x, bbDims.y, bbDims.z); + break; + case 'diagonal': + size = bbDims.length(); + break; + case 'volumeCubeRoot': + size = bbDims.x * bbDims.y * bbDims.z; + size = Math.pow(size, 1 / 3); + break; + default: + console.error('Unknown sizeBy ' + size); + } + return size; +}; + +Object3DUtil.getObjectStats = function(object3D, includeChildModelInstance) { + var nverts = 0; + var nfaces = 0; + var nmeshes = 0; + Object3DUtil.traverseMeshes(object3D, !includeChildModelInstance, function(mesh) { + nverts += GeometryUtil.getGeometryVertexCount(mesh.geometry); + nfaces += GeometryUtil.getGeometryFaceCount(mesh.geometry); + nmeshes += 1; + }); + return { nverts: nverts, nfaces: nfaces, nmeshes: nmeshes }; +}; + +Object3DUtil.getSurfaceArea = function(object3D, opts) { + opts = opts || {}; + var includeChildModelInstance = opts.includeChildModelInstance; + var transform = opts.transform; + var meshFilter = opts.meshFilter; + var triFilter = opts.triFilter; + object3D.updateMatrixWorld(); + var area = 0; + Object3DUtil.traverseMeshes(object3D, !includeChildModelInstance, function(mesh) { + if (!meshFilter || meshFilter(mesh)) { + var t = mesh.matrixWorld; + if (transform) { + t = transform.clone(); + t.multiply(mesh.matrixWorld); + } + area += GeometryUtil.getSurfaceAreaFiltered(mesh.geometry, t, triFilter); + } + }); + return area; +}; + +/** + * Takes an object3D and rotates so that its two vectors front and up align with targetUp and targetFront. + * Assumptions: objectUp perpendicular to objectFront, targetUp perpendicular to targetFront. + * @param object3D Object to align + * @param objectUp Object's semantic up vector + * @param objectFront Object's semantic front vector + * @param targetUp Target up vector + * @param targetFront Target front vector + */ +Object3DUtil.alignToUpFrontAxes = function (object3D, objectUp, objectFront, targetUp, targetFront) { + Object3DUtil.alignAxes(object3D, objectUp, objectFront, targetUp, targetFront); +}; + +/** + * Takes an object3D and rotates so that its two vectors u and v align with u and v. + * Assumptions: both sets of u and v are perpendicular. + * @param object3D Object to align + * @param objU Object's first vector + * @param objV Object's second vector + * @param tgtU Target first vector + * @param tgtV Target second vector + */ +Object3DUtil.alignAxes = function (object3D, objU, objV, tgtU, tgtV) { + // Unapply existing rotations (does this work? does object position end up not quite what we want...?) + object3D.rotation.set(0, 0, 0); + object3D.updateMatrix(); + var transform = Object3DUtil.getAlignmentMatrix(objU, objV, tgtU, tgtV); + // Apply this transform to matrix + object3D.applyMatrix(transform); + object3D.matrixWorldNeedsUpdate = true; // make sure matrixWorldNeedsUpdate is set + Object3DUtil.clearCache(object3D); +}; + +/** + * Returns matrix to align from objectUp/objectFront to targetUp/targetFront + * Assumptions: objectUp perpendicular to objectFront, targetUp perpendicular to targetFront. + * @param objectUp Object's semantic up vector + * @param objectFront Object's semantic front vector + * @param targetUp Target up vector + * @param targetFront Target front vector + */ +Object3DUtil.getAlignmentMatrix = function (objectUp, objectFront, targetUp, targetFront) { + // Figure out what transform to apply to matrix + var objM = Object3DUtil.axisPairToOrthoMatrix(objectUp, objectFront); + var targetM = Object3DUtil.axisPairToOrthoMatrix(targetUp, targetFront); + var transform = new THREE.Matrix4(); + var objMinv = new THREE.Matrix4(); + objMinv.getInverse(objM); + transform.multiplyMatrices(targetM, objMinv); + return transform; +}; + +/** + * Returns matrix to align from objectUp/objectFront to targetUp/targetFront + * Assumptions: objectUp perpendicular to objectFront, targetUp perpendicular to targetFront. + * @param objectUp Object's semantic up vector + * @param objectFront Object's semantic front vector + * @param targetUp Target up vector + * @param targetFront Target front vector + */ +Object3DUtil.getAlignmentQuaternion = function (objectUp, objectFront, targetUp, targetFront) { + var m = Object3DUtil.getAlignmentMatrix(objectUp, objectFront, targetUp, targetFront); + var position = new THREE.Vector3(); + var scale = new THREE.Vector3(); + var quaternion = new THREE.Quaternion(); + m.decompose( position, quaternion, scale ); + return quaternion; +}; + +Object3DUtil.axisPairToOrthoMatrix = function (_v1, _v2) { + // Let's make a copy so we don't change the incoming vectors + var v1 = new THREE.Vector3(); + v1.copy(_v1); + v1.normalize(); + var v2 = new THREE.Vector3(); + v2.copy(_v2); + v2.normalize(); + var v3 = new THREE.Vector3(); + v3.crossVectors(v1, v2); + + var m = new THREE.Matrix4(); + + m.set( + v1.x, v2.x, v3.x, 0, + v1.y, v2.y, v3.y, 0, + v1.z, v2.z, v3.z, 0, + 0, 0, 0, 1 + ); + + return m; +}; + +/** + * Takes the rotation for a rotated object3D that is aligned with targetUp and targetFront and returns the + * objectUp and objectFront that is needed to achieve that alignment + * Assumptions: targetUp perpendicular to targetFront. + * @param rotation Rotation of aligned object + * @param targetUp Target up vector + * @param targetFront Target front vector + * @param epsilon Epsilon to use to snap to integers + * @return "up": Object's semantic up vector + * @return "front": Object's semantic front vector + */ +Object3DUtil.getObjUpFrontAxes = function (rotation, targetUp, targetFront, epsilon) { + var targetM = Object3DUtil.axisPairToOrthoMatrix(targetUp, targetFront); + var m = new THREE.Matrix4(); + if (rotation instanceof THREE.Vector3) { + m.makeRotationFromEuler(new THREE.Euler(rotation.x, rotation.y, rotation.z)); + } else if (rotation instanceof THREE.Quaternion) { + m.makeRotationFromQuaternion(rotation); + } + var mInv = new THREE.Matrix4(); + mInv.getInverse(m); + var objM = new THREE.Matrix4(); + objM.multiplyMatrices(mInv, targetM); + var up = new THREE.Vector3(); + up.setFromMatrixColumn(objM, 0); + up = Object3DUtil.snapToInteger(up, epsilon); + var front = new THREE.Vector3(); + front.setFromMatrixColumn(objM, 1); + front = Object3DUtil.snapToInteger(front, epsilon); + return { 'up': up, 'front': front }; +}; + +Object3DUtil.getRotationForOrientingBBFace = function (unorientedBbFaceIndex, targetBbfaceIndex) { + // Get rotation matrix that takes the unorientedBbFaceIndex to be oriented in world space as the bbfaceIndex + var r = new THREE.Matrix3(); + if (unorientedBbFaceIndex !== targetBbfaceIndex) { + if (unorientedBbFaceIndex >= 0 && targetBbfaceIndex >= 0) { + console.debug('Need to orient ' + unorientedBbFaceIndex + ' to ' + targetBbfaceIndex); + if (targetBbfaceIndex !== Constants.BBoxFaces.BOTTOM && targetBbfaceIndex !== Constants.BBoxFaces.TOP && + unorientedBbFaceIndex !== Constants.BBoxFaces.BOTTOM && unorientedBbFaceIndex !== Constants.BBoxFaces.TOP) { + r = Object3DUtil.getAlignmentMatrix( + Object3DUtil.OutNormals[Constants.BBoxFaces.TOP], + Object3DUtil.OutNormals[unorientedBbFaceIndex], + Object3DUtil.OutNormals[Constants.BBoxFaces.TOP], + Object3DUtil.OutNormals[targetBbfaceIndex] + ); + } else { + r = Object3DUtil.getAlignmentMatrix( + Object3DUtil.OutNormals[unorientedBbFaceIndex], + Object3DUtil.OutNormals[(unorientedBbFaceIndex + 2) % 6], + Object3DUtil.OutNormals[targetBbfaceIndex], + Object3DUtil.OutNormals[(targetBbfaceIndex + 2) % 6] + ); + } + } else { + if (unorientedBbFaceIndex < 0) { + console.warn('Invalid unorientedBbFaceIndex: ' + unorientedBbFaceIndex); + } + if (targetBbfaceIndex < 0) { + console.warn('Invalid targetBbfaceIndex: ' + targetBbfaceIndex); + } + } + } + return r; +}; + +Object3DUtil.clearTransform = function(object3D) { + object3D.rotation.set(0, 0, 0); + object3D.scale.set(1, 1, 1); + object3D.position.set(0, 0, 0); + object3D.updateMatrix(); + Object3DUtil.clearCache(object3D); +}; + +Object3DUtil.normalize = function (object3D, alignmentMatrix, scaleVector) { + // Unapply existing rotations (does this work? does object position end up not quite what we want...?) + object3D.rotation.set(0, 0, 0); + object3D.scale.set(1, 1, 1); + object3D.position.set(0, 0, 0); + object3D.updateMatrix(); + var transform = alignmentMatrix; + // Apply this transform to matrix + object3D.applyMatrix(transform); + object3D.updateMatrix(); + object3D.matrixWorldNeedsUpdate = true; + + // Get bounding box... + var parentMatrixWorldInv; + if (object3D.parent) { + object3D.parent.updateMatrixWorld(); + parentMatrixWorldInv = new THREE.Matrix4(); + parentMatrixWorldInv.getInverse(object3D.parent.matrixWorld); + } + + if (scaleVector) { + object3D.scale.copy(scaleVector); + } + var bb = Object3DUtil.computeBoundingBox(object3D, parentMatrixWorldInv); + // Scale to be unit? +// var dims = bb.dimensions(); +// var maxDim = Math.max( dims.x, dims.y, dims.z ); +// object3D.scale.multiplyScalar(1.0/maxDim); +// bb = Object3DUtil.computeBoundingBox(object3D, parentMatrixWorldInv); + + var shift = new THREE.Vector3(); + shift.addVectors(bb.min, bb.max); + shift.multiplyScalar(-0.5); + + // Place object in center + object3D.position.copy(shift); + + object3D.updateMatrix(); + Object3DUtil.clearCache(object3D); +}; + +Object3DUtil.setChildAttachmentPoint = function (parent, child, p, pointCoordFrame) { + // Note: We want T1 * T2 = T1' T2' = T3 + // So T1' = T1 * T2 * (T2')^-1 + // For other child transforms: + // Want T1 * Tc = T1' * Tc' + // => = T1 * T2 * (T2')^-1 * Tc' + // => Tc' = T2' * (T2)^-1 * Tc + var oldModelBaseMatrixInv = new THREE.Matrix4(); + oldModelBaseMatrixInv.getInverse(child.matrix); + + var matrix = new THREE.Matrix4(); + matrix.multiplyMatrices(parent.matrix, child.matrix); + + // TODO: Should we be clearing the transform of the child here? + if (pointCoordFrame === 'worldBB') { + var worldBB = Object3DUtil.computeBoundingBox(child); + var wp = worldBB.getWorldPosition(p); + var pm = new THREE.Matrix4(); + pm.getInverse(child.matrixWorld); + var cp = wp.applyMatrix4(pm); + + Object3DUtil.clearTransform(child); + child.position.set(-cp.x, -cp.y, -cp.z); + child.updateMatrix(); + } else if (pointCoordFrame === 'parentBB') { + // NOTE: Unchecked logic!!! + var rot = new THREE.Matrix4(); + rot.makeRotationFromQuaternion(parent.quaternion); + var modelBB = Object3DUtil.computeBoundingBoxLocal(child, rot); + var cp = modelBB.getWorldPosition(p); + + Object3DUtil.clearTransform(child); + child.quaternion.copy(parent.quaternion); + child.position.set(-cp.x, -cp.y, -cp.z); + child.updateMatrix(); + } else if (pointCoordFrame === 'childBB') { + var modelBB = Object3DUtil.computeBoundingBoxLocal(child); + var cp = modelBB.getWorldPosition(p); + + Object3DUtil.clearTransform(child); + child.position.set(-cp.x, -cp.y, -cp.z); + child.updateMatrix(); + } else { + // Assume world coordinate frame + if (pointCoordFrame !== 'child') { + console.error('setChildAttachmentPoint invalid coord frame: ' + pointCoordFrame + ', using child'); + pointCoordFrame = 'child'; + } + var wp = p.clone(); + + child.position.set(-wp.x, -wp.y, -wp.z); + child.updateMatrix(); + } + + // Convert this.modelBaseObject3D to use the specified attachmentPoint + var modelBaseObject3DInv = new THREE.Matrix4(); + modelBaseObject3DInv.getInverse(child.matrix); + matrix.multiply(modelBaseObject3DInv); + + Object3DUtil.setMatrix(parent, matrix); + child.userData['attachmentPoint'] = p; + child.userData['attachmentPointCoordFrame'] = pointCoordFrame; + + // Adjust other children of object3D + matrix.multiplyMatrices(child.matrix, oldModelBaseMatrixInv); + for (var i = 0; i < parent.children.length; i++) { + var c = parent.children[i]; + if (child !== c) { + c.applyMatrix(matrix); + c.matrixWorldNeedsUpdate = true; + } + } + //parent.updateMatrix(); + //parent.updateMatrixWorld(); +}; + +Object3DUtil.setMatrix = function (obj, matrix) { + obj.matrix = matrix; + obj.matrix.decompose(obj.position, obj.quaternion, obj.scale); + obj.matrixWorldNeedsUpdate = true; // make sure matrixWorldNeedsUpdate is set + Object3DUtil.clearCache(obj); +}; + +Object3DUtil.snapToInteger = function (v, epsilon) { + if (epsilon) { + if (v instanceof THREE.Vector3) { + return new THREE.Vector3( + Object3DUtil.snapToInteger(v.x, epsilon), + Object3DUtil.snapToInteger(v.y, epsilon), + Object3DUtil.snapToInteger(v.z, epsilon) + ); + } else if (typeof v === 'number') { + var rounded = Math.round(v); + return (Math.abs(rounded - v) < epsilon) ? rounded : v; + } + } + return v; +}; + +// Helper functions for rotating objects +Object3DUtil.rotateObject3DEuler = function (obj, delta, order, stationaryBbBoxPoint) { + obj.updateMatrixWorld(); + var bb = Object3DUtil.getBoundingBox(obj); + var base = bb.getWorldPosition(stationaryBbBoxPoint); + Object3DUtil.placeObject3D(obj, new THREE.Vector3(), stationaryBbBoxPoint); + + var q = new THREE.Quaternion(); + q.setFromEuler(new THREE.Euler(delta.x, delta.y, delta.z, order), true); + obj.quaternion.multiply(q); + obj.updateMatrix(); + + Object3DUtil.clearCache(obj); + Object3DUtil.placeObject3D(obj, base, stationaryBbBoxPoint); +}; + +Object3DUtil.rotateObject3DAboutAxis = function (obj, axis, delta, stationaryBbBoxPoint) { + //console.time('rotateObject3DAboutAxis'); + obj.updateMatrixWorld(); + var bb = Object3DUtil.getBoundingBox(obj); + var base = bb.getWorldPosition(stationaryBbBoxPoint); + Object3DUtil.placeObject3D(obj, new THREE.Vector3(), stationaryBbBoxPoint); + + var qwi = obj.getWorldQuaternion().inverse(); + var localAxis = axis.clone().applyQuaternion(qwi); + var q = new THREE.Quaternion(); + q.setFromAxisAngle(localAxis, delta); + obj.quaternion.multiply(q); + obj.updateMatrix(); + + Object3DUtil.clearCache(obj); + Object3DUtil.placeObject3D(obj, base, stationaryBbBoxPoint); + //console.timeEnd('rotateObject3DAboutAxis'); +}; + +Object3DUtil.rotateObject3DWrtBBFace = function (obj, axis, delta, bbface) { + if (bbface === undefined) { + bbface = Constants.BBoxFaceCenters.BOTTOM; + } + var stationaryBbBoxPoint = Object3DUtil.FaceCenters01[bbface]; + Object3DUtil.rotateObject3DAboutAxis(obj, axis, delta, stationaryBbBoxPoint); +}; + +Object3DUtil.rotateObject3DAboutAxisSimple = function (obj, axis, delta, isWorld) { + //console.time('rotateObject3DAboutAxisSimple'); + var localAxis = axis; + if (isWorld) { + var qwi = obj.getWorldQuaternion().inverse(); + localAxis = axis.clone().applyQuaternion(qwi); + } + + var q = new THREE.Quaternion(); + q.setFromAxisAngle(localAxis, delta); + obj.quaternion.multiplyQuaternions(obj.quaternion, q); + obj.updateMatrix(); + Object3DUtil.clearCache(obj); + //console.timeEnd('rotateObject3DAboutAxisSimple'); +}; + +// Helper functions for rotating objects +Object3DUtil.applyAlignment = function (obj, alignment, stationaryBbBoxPoint) { + obj.updateMatrixWorld(); + var bb = Object3DUtil.getBoundingBox(obj); + var base = bb.getWorldPosition(stationaryBbBoxPoint); + Object3DUtil.placeObject3D(obj, new THREE.Vector3(), stationaryBbBoxPoint); + + var transform = alignment; + // Apply this transform to matrix + obj.applyMatrix(transform); + obj.matrixWorldNeedsUpdate = true; // make sure matrixWorldNeedsUpdate is set + Object3DUtil.clearCache(obj); + + Object3DUtil.placeObject3D(obj, base, stationaryBbBoxPoint); +}; + +// Helper functions for centering and rescaling object3ds +Object3DUtil.centerAndRescaleObject3DToWorld = function (obj, targetSize, centerTo, bbBoxPointToCenter) { + targetSize = targetSize || 80; + var bb = Object3DUtil.getBoundingBox(obj); + var bbSize = bb.dimensions(); + var scale = targetSize / bbSize.length(); + obj.scale.x = obj.scale.y = obj.scale.z = scale; + obj.updateMatrix(); + centerTo = centerTo || new THREE.Vector3(); + Object3DUtil.clearCache(obj); + Object3DUtil.placeObject3D(obj, centerTo, bbBoxPointToCenter); +}; + + +/* placeObject3D takes a THREE.Object3D, a target position (THREE.Vector3), and relative position + (THREE.Vector3 in the object3D's world bbox coordinate frame with min = [0,0,0] and max = [1,1,1]) to position. + The function places the object such that the bbBoxPointToPosition when transformed to world coordinates + matches the specified position */ +Object3DUtil.placeObject3D = function (obj, targetWorldPosition, bbBoxPointToPosition) { + if (!targetWorldPosition) { + targetWorldPosition = new THREE.Vector3(0, 0, 0); + } + if (!bbBoxPointToPosition) { + bbBoxPointToPosition = new THREE.Vector3(0.5, 0.5, 0.5); + } + + var bb = Object3DUtil.getBoundingBox(obj); + var currentWorldPosition = bb.getWorldPosition(bbBoxPointToPosition); + + var shift; + if (obj.parent) { + obj.parent.updateMatrixWorld(); + var current = obj.parent.worldToLocal(currentWorldPosition.clone()); + var target = obj.parent.worldToLocal(targetWorldPosition.clone()); + shift = target.clone(); + shift.sub(current); + } else { + shift = targetWorldPosition.clone(); + shift.sub(currentWorldPosition); + } + obj.position.add(shift); + obj.updateMatrix(); + Object3DUtil.clearCache(obj); +}; + +Object3DUtil.placeObject3DByOrigin = function (obj, targetWorldPosition) { + var shift; + var currentWorldPosition = obj.localToWorld(new THREE.Vector3()); + if (obj.parent) { + obj.parent.updateMatrixWorld(); + var current = obj.parent.worldToLocal(currentWorldPosition.clone()); + var target = obj.parent.worldToLocal(targetWorldPosition.clone()); + shift = target.clone(); + shift.sub(current); + } else { + shift = targetWorldPosition.clone(); + shift.sub(currentWorldPosition); + } + obj.position.add(shift); + obj.updateMatrix(); + Object3DUtil.clearCache(obj); +}; + +/* placeObject3DByBBFaceCenter takes a THREE.Object3D, a THREE.Vector3, and an int + demarking the face of which the center will be used as a reference point for placement. + Appropriate values can be found in the Constants.BBoxFaceCenters enum. The function + places the object such that the center of the chosen face is at the given location */ +Object3DUtil.placeObject3DByBBFaceCenter = function (obj, targetWorldPosition, bboxFaceCenterIndex) { + var bbBoxPointToPosition = Object3DUtil.FaceCenters01[bboxFaceCenterIndex]; + Object3DUtil.placeObject3D(obj, targetWorldPosition, bbBoxPointToPosition); +}; + +Object3DUtil.getBBoxFaceCenter = function (obj, bboxFaceCenterIndex) { + // Clone center so it's okay in case it gets mutated. + var centers = Object3DUtil.getBoundingBox(obj).getFaceCenters(); + return centers[bboxFaceCenterIndex].clone(); +}; + +Object3DUtil.tumble = function (obj, bboxFaceCenterIndex) { + if (!obj.userData) { + obj.userData = {}; + } + var tumbleIndex = (obj.userData['tumbleIndex'] + 1) % 3; + var transform = Object3DUtil.getAlignmentMatrix( + new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), + new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)); + var bboxFaceCenter; + if (bboxFaceCenterIndex !== undefined) { + bboxFaceCenter = Object3DUtil.FaceCenters01[bboxFaceCenterIndex]; + } else { + // TODO: Should we have this as the default? + bboxFaceCenter = Object3DUtil.FaceCenters01[Constants.BBoxFaceCenters.BOTTOM]; + } + Object3DUtil.applyAlignment(obj, transform, bboxFaceCenter); + obj.userData['tumbleIndex'] = tumbleIndex; +}; + +Object3DUtil.rescaleObject3D = function (obj, scale) { + obj.scale.x = obj.scale.y = obj.scale.z = scale; + obj.updateMatrix(); + Object3DUtil.clearCache(obj); +}; + +Object3DUtil.rescaleObject3DToFit = function (obj, dim) { + var bbSize = Object3DUtil.getBoundingBoxDims(obj); + var scale; + if ((typeof dim) === 'number') { + scale = dim / bbSize.length(); + } else { + var scaleV = new THREE.Vector3(); + scaleV.copy(dim); + scaleV.divide(bbSize); + scale = Math.min(scaleV.x, scaleV.y, scaleV.z); + } + obj.scale.multiplyScalar(scale); + //obj.scale.x = obj.scale.y = obj.scale.z = scale; + obj.updateMatrix(); + Object3DUtil.clearCache(obj); +}; + +Object3DUtil.makeSymmetryPlane = function (bb, planeType, normal, dist, planeColor, transparency) { + var c = (planeColor !== undefined) ? planeColor : 0x0000A0; + var faceMat = new THREE.MeshPhongMaterial({ + color: c, + specular: c, + //ambient: c, + emissive: c, + side: THREE.DoubleSide, + opacity: 1 + }); + if (transparency === undefined) { + transparency = 0.6; + } + if (transparency > 0.0) { + faceMat.opacity = 1 - transparency; + faceMat.transparent = true; + } + var dims = bb.dimensions(); + var width, height, u, v; + switch (planeType) { + case 'X': + width = dims.y; + height = dims.z; + v = new THREE.Vector3(0, 0, 1); + u = new THREE.Vector3(1, 0, 0); + break; + case 'Y': + width = dims.x; + height = dims.z; + v = new THREE.Vector3(0, 0, 1); + u = new THREE.Vector3(0, 1, 0); + break; + case 'Z': + width = dims.x; + height = dims.y; + v = new THREE.Vector3(0, 1, 0); + u = new THREE.Vector3(0, 0, 1); + break; + case 'nd': + // TODO(MS): Unhack + width = 10 * dims.x; + height = 10 * dims.y; + var w = normal.clone(); + // Get random perpendicular to normal + var rand = new THREE.Vector3(RNG.global.random(), RNG.global.random(), RNG.global.random()); + rand = rand.normalize(); + var randLengthInNormalDir = rand.dot(normal); + var randPartInNormalDir = w.multiplyScalar(randLengthInNormalDir); + u = normal; + v = rand.clone(); + v.sub(randPartInNormalDir); + v.normalize(); + //v = new THREE.Vector3(); + //v = v.crossVectors(normal, u); + //console.log(normal); + //console.log(u); + //console.log(v); + break; + default: + return null; + } + + var planeU = new THREE.Vector3(0, 0, 1); + var planeV = new THREE.Vector3(0, 1, 0); + var geometryPlane = new THREE.PlaneGeometry(width, height, 1, 1); + var meshPlane = new THREE.Mesh(geometryPlane, faceMat); + Object3DUtil.alignToUpFrontAxes(meshPlane, planeU, planeV, u, v); + Object3DUtil.placeObject3D(meshPlane, bb.centroid()); + return meshPlane; +}; + +Object3DUtil.makeGrid = function (width, height, numHGridLines, numVGridLines, faceColor) { + + var c = faceColor; + if (!c) c = 0xdadada; + + var MAX_X = width / 2; + var MIN_X = 0 - (width / 2); + var MAX_Y = height / 2; + var MIN_Y = 0 - (height / 2); + + var blockSizeH = height / numHGridLines; + var blockSizeV = width / numVGridLines; + + var epsilon = 2; + + var geometryH = new THREE.Geometry(); + geometryH.vertices.push(new THREE.Vector3(MIN_X, MAX_Y, epsilon)); + geometryH.vertices.push(new THREE.Vector3(MAX_X, MAX_Y, epsilon)); + + var geometryV = new THREE.Geometry(); + geometryV.vertices.push(new THREE.Vector3(MIN_X, MIN_Y, epsilon)); + geometryV.vertices.push(new THREE.Vector3(MIN_X, MAX_Y, epsilon)); + + var lineMat = new THREE.LineBasicMaterial({ + color: 0xa0a0a0, + opacity: 1 + }); + + var faceMat = new THREE.MeshBasicMaterial({ + color: c, //0x575145, + opacity: 1 + }); + var geometryPlane = new THREE.PlaneBufferGeometry(width, height, 1, 1); + var meshPlane = new THREE.Mesh(geometryPlane, faceMat); + + var object3D = new THREE.Object3D(); + + for (var iy = 0; iy <= numHGridLines; iy++) { + var lineX = new THREE.Line(geometryH, lineMat); + lineX.position.y = -iy * blockSizeH; + object3D.add(lineX); + } + for (var ix = 0; ix <= numVGridLines; ix++) { + var lineY = new THREE.Line(geometryV, lineMat); + lineY.position.x = ix * blockSizeV; + object3D.add(lineY); + } + + object3D.add(meshPlane); + object3D.name = 'Grid'; + object3D.userData.totalWidth = width; + object3D.userData.totalHeight = height; + object3D.userData.gridWidth = blockSizeV; + object3D.userData.gridHeight = blockSizeH; + object3D.userData.gridColor = faceColor; + + return object3D; +}; + +Object3DUtil.makeAxes = function (axisLength) { + + if (!axisLength) axisLength = 1*Constants.metersToVirtualUnit; + + var axes = new THREE.Object3D(); + + //Shorten the vertex function + function v(x, y, z) { + return new THREE.Vector3(x, y, z); + } + + //Create axis (point1, point2, colour) + function createAxis(p1, p2, color) { + var line, lineGeometry = new THREE.Geometry(), + lineMat = new THREE.LineBasicMaterial({color: color, linewidth: 10}); + lineGeometry.vertices.push(p1, p2); + line = new THREE.Line(lineGeometry, lineMat); + axes.add(line); + } + + createAxis(v(0, 0, 0), v(axisLength, 0, 0), 0xFF0000); + createAxis(v(0, 0, 0), v(0, axisLength, 0), 0x00FF00); + createAxis(v(0, 0, 0), v(0, 0, axisLength), 0x0000FF); + + return axes; +}; + +Object3DUtil.makeGround = function (width, height, color) { + var geometry = new THREE.PlaneBufferGeometry(width, height); + //geometry.verticesArray = geometry.attributes['position'].array; + geometry.computeFaceNormals(); + var planeMaterial = new THREE.MeshBasicMaterial({ color: color || 0xffffff }); + //planeMaterial.ambient = planeMaterial.color; + //planeMaterial.side = THREE.DoubleSide; + var ground = new THREE.Mesh(geometry, planeMaterial); + ground.castShadow = false; + ground.receiveShadow = true; + + var object3D = new THREE.Object3D(); + object3D.add(ground); + + Object3DUtil.alignToUpFrontAxes(object3D, new THREE.Vector3(0, 0, 1), new THREE.Vector3(1, 0, 0), Constants.worldUp, Constants.worldFront); + + return object3D; +}; + +Object3DUtil.makePickingPlane = function (width, height) { + var geometry = new THREE.PlaneBufferGeometry(width, height); + geometry.computeFaceNormals(); + var planeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide, visible: false }); + var ground = new THREE.Mesh(geometry, planeMaterial); + ground.name = 'PickingPlane'; + ground.userData.width = width; + ground.userData.heigth = height; + Object3DUtil.alignToUpFrontAxes(ground, new THREE.Vector3(0, 0, 1), new THREE.Vector3(1, 0, 0), Constants.worldUp, Constants.worldFront); + return ground; +}; + +Object3DUtil.createGroundAndFog = function (scene) { + var geometry = new THREE.PlaneBufferGeometry(100, 100); + var planeMaterial = new THREE.MeshPhongMaterial({color: 0xffdd99}); + planeMaterial.color.offsetHSL(0, 0, 0.9); + //planeMaterial.ambient = planeMaterial.color; + var ground = new THREE.Mesh(geometry, planeMaterial); + ground.scale.set(100, 100, 100); + ground.castShadow = false; + ground.receiveShadow = true; + scene.add(ground); + + scene.fog = new THREE.Fog(0xffaa55, 1000, 3000); + scene.fog.color.offsetHSL(0.02, -0.15, -0.65); +}; + +Object3DUtil.getBBoxForModelInstanceArray = function (models) { + var r = new BBox(); + for (var i = 0; i < models.length; i++) { + r.includeObject3D(models[i].model.object3D); + } + r.bboxDims = r.dimensions(); + r.width = r.bboxDims.x; + r.height = r.bboxDims.z; + r.depth = r.bboxDims.y; + + return r; +}; + +Object3DUtil.getColor = function (value) { + return Colors.toColor(value); +}; + +Object3DUtil.createColor = function (id, palette) { + return Colors.createColor(id, palette || Constants.defaultPalette); +}; + +Object3DUtil.getBasicMaterial = function (color, alpha) { + return Materials.getBasicMaterial(color, alpha); +}; + +Object3DUtil.getMaterial = function (mat) { + return Materials.toMaterial(mat); +}; + +Object3DUtil.getStandardMaterial = function (color, alpha) { + return Materials.getStandardMaterial(color, alpha); +}; + +Object3DUtil.getSimpleFalseColorMaterial = function (id, color, palette) { + return Materials.getSimpleFalseColorMaterial(id, color, palette); +}; + +Object3DUtil.addSimple2LightSetup = function (scene, position, doShadowMap) { + position = position || new THREE.Vector3(-100, 100, 100); + var ambient = new THREE.AmbientLight(0x050505); + + var light0 = new THREE.PointLight(0xdadacd, 0.85); + var p0 = new THREE.Vector3(); + p0.copy(position); + light0.position.copy(p0); + var light1 = new THREE.PointLight(0x030309, 0.03); + var p1 = new THREE.Vector3(); + p1.copy(position); + p1.negate(); + light1.position.copy(p1); + + if (doShadowMap) { + var light = Object3DUtil.createSpotLightShadowMapped(1000); + light.position.copy(light0.position); + light.onlyShadow = true; + scene.add(light); + } + + scene.add(ambient); + scene.add(light0); + scene.add(light1); +}; + +Object3DUtil.createSpotLightShadowMapped = function (lightBoxSize) { + var light = new THREE.SpotLight(0xffffff, 1, 0, Math.PI, 1); + light.target.position.set(0, 0, 0); + + light.castShadow = true; + + light.shadowCameraNear = 1; + light.shadowCameraFar = lightBoxSize; + light.shadowCameraRight = lightBoxSize; + light.shadowCameraLeft = -lightBoxSize; + light.shadowCameraTop = lightBoxSize; + light.shadowCameraBottom = -lightBoxSize; + light.shadowCameraFov = 50; + + light.shadowBias = 0.0001; + light.shadowDarkness = 0.5; + + light.shadowMapWidth = 2048; + light.shadowMapHeight = 2048; + + light.shadowCameraVisible = true; + + return light; +}; + +Object3DUtil.getTotalDims = function (models) { + + var total = new THREE.Vector3(0, 0, 0); + + for (var k = 0; k < models.length; k++) { + var dims = models[k].getBBox().dimensions(); + total.add(dims); + } + + return total; +}; + +Object3DUtil.getSortedModels = function (models, sizeBy) { + if (!sizeBy) sizeBy = 'height'; + + var sizes = []; + for (var k = 0; k < models.length; k++) { + var dims = models[k].getBBox().dimensions(); + sizes[k] = Object3DUtil.convertBbDimsToSize(dims, sizeBy); + } + var sortedIndices = _.sortWithIndices(sizes).sortedIndices; + var sortedModels = []; + for (var i = 0; i < sortedIndices.length; i++) { + sortedModels[i] = models[sortedIndices[i]]; + } + return sortedModels; +}; + +Object3DUtil.lineup = function (models, params) { + params = params || {}; + var y = params.y || 0; + // Spacing between objects + var objectSpacing = params.objectSpacing || 0.00; + // Ratio for computing spacing between objects (as parameter of total width) + var objectSpacingRatio = params.objectSpacingRatio || 0.05; + + // Initialize + var l = {}; + l.nModels = models.length; + var bbBoxRefPoint = new THREE.Vector3(0.5, 0.5, 0); + l.widths = []; + l.heights = []; + l.depths = []; + l.maxDepth = 0; + l.medianHeight = -1; + l.sumWidth = 0; + l.sumWidthWithGaps = 0; + + // Sort models by height + var sizes = []; + for (var k = 0; k < models.length; k++) { + var dims = models[k].getBBox().dimensions(); + sizes[k] = dims.z; + } + l.sortedIndices = _.sortWithIndices(sizes).sortedIndices; + + // Loop over models and save dims + for (var i = 0; i < l.nModels; i++) { + var bbox = models[l.sortedIndices[i]].getBBox(); + var w = bbox.dimensions().x; + var d = bbox.dimensions().y; + var h = bbox.dimensions().z; + l.widths[i] = w; + l.heights[i] = h; + l.depths[i] = d; + if (d > l.maxDepth) l.maxDepth = d; + l.sumWidth += w; + } + + l.medianHeight = _.sortWithIndices(l.heights)[Math.round(models.length / 2)]; + var gapx = Math.max( l.sumWidth*objectSpacingRatio, objectSpacing ); + var sumWidthWithGaps = l.sumWidth + (l.nModels - 1)*gapx; + var epsilon = l.sumWidth * 0.01; + var startX = -sumWidthWithGaps / 2; + var currCenter = new THREE.Vector3(startX, y + l.maxDepth / 2, 0); + + // Lineup in order from left to right + for (var j = 0; j < l.nModels; j++) { + var model = models[l.sortedIndices[j]]; + var halfWidthPlus = (l.widths[j] + epsilon) / 2; + currCenter.x += halfWidthPlus; + Object3DUtil.placeObject3D(model.object3D, currCenter, bbBoxRefPoint); + currCenter.x += halfWidthPlus; + } + + return l; +}; + +Object3DUtil.matrix4ToProto = function (m) { + var array = []; + for (var j = 0; j < 16; j++) { + array.push(m.elements[j]); + } + var transform = { + rows: 4, + cols: 4, + data: array + }; + return transform; +}; + +Object3DUtil.vectorToString = function (v) { + return v.x + ',' + v.y + ',' + v.z; +}; + +Object3DUtil.quaternionToString = function (v) { + return v.x + ',' + v.y + ',' + v.z + "," + v.w; +}; + +Object3DUtil.toVector2 = function (v) { + if (v) { + if (v instanceof THREE.Vector2) { + return v; + } else if (typeof v === 'string') { + // parse 0,0 into two pieces + v = v.trim(); + if (v) { + if ((v.startsWith('[') && v.endsWith(']')) || (v.startsWith('(') && v.endsWith(')'))) { + v = v.substring(1, v.length - 1); + } + v = _.replaceAll(v, '\\,', ','); + var p = v.split(/\s*,\s*/, 2).map( function(x) { return parseFloat(x); }); + if (p.length === 2) { + return new THREE.Vector2(p[0], p[1]); + } else { + console.error('Cannot convert object to Vector2', v); + return null; + } + } + } else if (v.x != undefined && v.y != undefined) { + return new THREE.Vector2(v.x, v.y); + } else if (v.count || v.length) { + return new THREE.Vector2(v[0], v[1]); + } else { + console.error('Cannot convert object to Vector2', v); + return null; + } + } +}; + +Object3DUtil.toVector3 = function (v) { + if (v) { + if (v instanceof THREE.Vector3) { + return v; + } else if (typeof v === 'string') { + // parse 0,0,0 into three pieces + v = v.trim(); + if (v) { + if ((v.startsWith('[') && v.endsWith(']')) || (v.startsWith('(') && v.endsWith(')'))) { + v = v.substring(1, v.length - 1); + } + v = _.replaceAll(v, '\\,', ','); + var p = v.split(/\s*,\s*/, 3).map( function(x) { return parseFloat(x); }); + if (p.length === 3) { + return new THREE.Vector3(p[0], p[1], p[2]); + } else { + console.error('Cannot convert object to Vector3', v); + return null; + } + } + } else if (v.x != undefined && v.y != undefined && v.z != undefined) { + return new THREE.Vector3(v.x, v.y, v.z); + } else if (v.count || v.length) { + return new THREE.Vector3(v[0], v[1], v[2]); + } else { + console.error('Cannot convert object to Vector3', v); + return null; + } + } +}; + +Object3DUtil.toQuaternion = function (v) { + if (v) { + if (v instanceof THREE.Quaternion) { + return v; + } else if (typeof v === 'string') { + // parse 0,0,0,0 into four pieces + v = v.trim(); + if (v) { + if ((v.startsWith('[') && v.endsWith(']')) || (v.startsWith('(') && v.endsWith(')'))) { + v = v.substring(1, v.length - 1); + } + v = _.replaceAll(v, '\\,', ','); + var p = v.split(/\s*,\s*/, 4).map( function(x) { return parseFloat(x); }); + if (p.length === 3) { + return new THREE.Quaternion(p[0], p[1], p[2], p[3]); + } else { + console.error('Cannot convert object to Quaternion', v); + return null; + } + } + } else if (v.x != undefined && v.y != undefined && v.z != undefined && v.w != undefined) { + return new THREE.Quaternion(v.x, v.y, v.z, v.w); + } else if (v.count || v.length) { + return new THREE.Quaternion(v[0], v[1], v[2], v[3]); + } else { + console.error('Cannot convert object to Quaternion', v); + return null; + } + } +}; + +Object3DUtil.toBBox = function(b) { + if (b) { + if (b instanceof BBox) { + return b; + } else if (b.min && b.max) { + return new BBox(Object3DUtil.toVector3(b.min), Object3DUtil.toVector3(b.max)); + } else { + console.error('Cannot convert object to BBox', b); + return null; + } + } +} + +Object3DUtil.toBox2 = function(b) { + if (b) { + if (b instanceof THREE.Box2) { + return b; + } else if (b.min && b.max) { + return new BBox(Object3DUtil.toVector3(b.min), Object3DUtil.toVector3(b.max)); + } else { + console.error('Cannot convert object to Box2', b); + return null; + } + } +} + + +Object3DUtil.toBox3 = function(b) { + if (b) { + if (b instanceof THREE.Box3) { + return b; + } else if (b.min && b.max) { + return new BBox(Object3DUtil.toVector2(b.min), Object3DUtil.toVector2(b.max)); + } else { + console.error('Cannot convert object to Box3', b); + return null; + } + } +} + +Object3DUtil.arrayToMatrix4 = function(m, isRowMajor) { + var matrix = new THREE.Matrix4(); + if (m.length === 16) { + if (isRowMajor) { + matrix.set( + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15]); + } else { + matrix.set( + m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15]); + } + } else if (m.length === 9) { + if (isRowMajor) { + matrix.set( + m[0], m[1], m[2], 0, + m[3], m[4], m[5], 0, + m[6], m[7], m[8], 0, + 0, 0, 0, 1); + } else { + matrix.set( + m[0], m[3], m[6], 0, + m[1], m[4], m[7], 0, + m[2], m[5], m[8], 0, + 0, 0, 0, 1); + } + } else { + console.warn('Invalid array length: ' + m.length); + } + return matrix; +}; + +Object3DUtil.isColinearVec2 = function(p1,p2,p3) { + var a = Math.abs((p1.y - p2.y) * (p1.x - p3.x) - (p1.y - p3.y) * (p1.x - p2.x)); + return (a < 0.00000001); +}; + +Object3DUtil.isColinearVec3 = function(p1,p2,p3) { + // Check if cross product is 0 + var v12 = new THREE.Vector3(); + v12.subVectors(p1, p2); + var v13 = new THREE.Vector3(); + v13.subVectors(p1, p3); + var z = new THREE.Vector3(); + z.crossVectors(v12, v13); + return (z.lengthSq() < 0.00000001); +}; + +Object3DUtil.stringifyReplacer = function (key, value) { + if (value instanceof THREE.Material) { + return Materials.getMaterialParams(value); + } else if (value instanceof THREE.Texture) { + return Materials.getTextureParams(value); + } else if (value instanceof THREE.Color) { + return value.getHex().toString(16); + } else { + return value; + } +}; + +Object3DUtil.toObjStr = function (prefix, v) { + var p = prefix; + if (v instanceof THREE.Vector3) { + return p + ' ' + v.x + ' ' + v.y + ' ' + v.z; + } else if (v instanceof Array) { + return p + ' ' + v.join(' '); + } else { + return null; + } +}; + + +Object3DUtil.loadTexture = function (url, mapping, onLoad, onError) { + return Materials.loadTexture({ url: url, mapping: mapping, onLoad: onLoad, onError: onError }); +}; + +// DFS traversal of node +// callback1 is applied before children are visited (children are visited only if callback1 returns true) +// callback2 is applied after children are visited +Object3DUtil.traverse = function (node, callback1, callback2) { + var processChildren = callback1(node); + if (processChildren) { + for (var i = 0, l = node.children.length; i < l; i++) { + Object3DUtil.traverse(node.children[i], callback1, callback2); + } + } + if (callback2) { + callback2(node); + } +}; + +Object3DUtil.traverseMeshes = function (object3D, nonrecursive, callback) { + Object3DUtil.traverse( + object3D, + function (node) { + if (node instanceof THREE.Mesh) { + callback(node); + return true; + } else if (node instanceof THREE.Object3D) { + if (object3D === node) return true; + else if (nonrecursive) { + // Skip if has modelInstance + if ((node.metadata && node.metadata.modelInstance) || (node.userData && node.userData.hasOwnProperty('objectIndex'))) { + return false; + } + } + } + return true; + }); +}; + +Object3DUtil.traverseVisibleMeshes = function (object3D, nonrecursive, callback) { + Object3DUtil.traverse( + object3D, + function (node) { + if (node instanceof THREE.Mesh) { + if (node.visible) { + callback(node); + return true; + } + } else if (node instanceof THREE.Object3D) { + if (object3D === node) return node.visible; + else if (nonrecursive) { + // Skip if has modelInstance + if ((node.metadata && node.metadata.modelInstance) || (node.userData && node.userData.hasOwnProperty('objectIndex'))) { + return false; + } + } + } + return node.visible; + }); +}; + +Object3DUtil.existsMesh = function (object3D, nonrecursive, callback) { + var exists = false; + Object3DUtil.traverse( + object3D, + function (node) { + if (node instanceof THREE.Mesh) { + exists = callback(node); + return !exists; + } else if (node instanceof THREE.Object3D) { + if (object3D === node) return true; + else if (nonrecursive) { + // Skip if has modelInstance + if ((node.metadata && node.metadata.modelInstance) || (node.userData && node.userData.hasOwnProperty('objectIndex'))) { + return false; + } + } + } + return !exists; + }); + return exists; +}; + +Object3DUtil.getIndexedObject3Ds = function (object3D) { + var objects = []; + Object3DUtil.traverse( + object3D, + function (node) { + if (node instanceof THREE.Object3D) { + if (node.userData && node.userData.hasOwnProperty('objectIndex')) { + objects[node.userData['objectIndex']] = node; + } + } + return true; + }); + return objects; +}; + +Object3DUtil.traverseModelInstances = function (modelInstance, callback) { + Object3DUtil.traverse( + modelInstance.object3D, + function (node) { + if (node instanceof THREE.Object3D) { + if (modelInstance.object3D === node) return true; + if (node.metadata && node.metadata.modelInstance) { + callback(node.metadata.modelInstance); + } + } + return true; + }); +}; + +Object3DUtil.isDescendantOf = function (candidate, object3D) { + var isDesc = false; + if (object3D instanceof Array) { + for (var i = 0; i < object3D.length; i++) { + isDesc = Object3DUtil.isDescendantOf(candidate, object3D[i]); + if (isDesc) { + break; + } + } + } else { + Object3DUtil.traverse( + object3D, + function (node) { + if (candidate === node) { + isDesc = true; + return false; + } else { + return true; + } + } + ); + } + return isDesc; +}; + +Object3DUtil.deepClone = function(object3D) { + // Deep clone that makes a copy of the geometry as well + var clone = object3D.clone(); + clone.traverse(function (node) { + if (node instanceof THREE.Mesh) { + var newGeom = GeometryUtil.clone(node.geometry); + node.geometry = newGeom; + } + }); + return clone; +}; + +Object3DUtil.copyObjectWithModelInstances = function (origObject, modelInstances, keepOldIndices, indexField) { + indexField = indexField || 'objectIndex'; + // Clone object + Object3DUtil.revertMaterials(origObject); + origObject.updateMatrixWorld(); + var clonedObject = origObject.clone(); + + // we want the cloned object to be detached, so set its matrix transform to its matrixWorld + // and update position, quaternion and scale so the matrix is retained. + clonedObject.matrix = clonedObject.matrixWorld.clone(); + clonedObject.matrix.decompose(clonedObject.position, clonedObject.quaternion, clonedObject.scale); + clonedObject.updateMatrixWorld(); + + // Make sure attached model instance information is properly copied as well + var clonedModelInstances = []; + Object3DUtil.traverse(clonedObject, function (node) { + if (node.userData.type === 'ModelInstance') { + var objectIndex = node.userData[indexField]; + var modelInstance = modelInstances[objectIndex].clone(node); + node.metadata = {modelInstance: modelInstance}; + + var nextIndex = (keepOldIndices)? objectIndex : clonedModelInstances.length; + modelInstance.index = nextIndex; + node.index = nextIndex; + node.userData[indexField] = nextIndex; + clonedModelInstances[nextIndex] = modelInstance; + } + return true; + }); + + return { + object: clonedObject, + modelInstances: clonedModelInstances + }; +}; + +Object3DUtil.findModelInstances = function (object3D, modelInstances, indexField) { + modelInstances = modelInstances || []; + + Object3DUtil.traverse(object3D, function (node) { + if (node.userData.type === 'ModelInstance') { + if (indexField) { + node.userData[indexField] = modelInstances.length; + } + modelInstances.push(node.metadata.modelInstance); + } + return true; + }); + + return modelInstances; +}; + +Object3DUtil.detachModelInstances = function (modelInstance, detachAll, scene) { + var modelInstances = Object3DUtil.findModelInstances(modelInstance.object3D); + var detached = []; + for (var i = 0; i < modelInstances.length; i++) { + var mi = modelInstances[i]; + if (mi.index !== modelInstance.index) { + var parent = mi.object3D.parent; + var parentInstance = Object3DUtil.getModelInstance(parent); + var detach = detachAll || parentInstance.index === mi.index; + if (detach) { + Object3DUtil.detachFromParent(mi.object3D, scene); + detached.push(mi); + } + } + } + return detached; +}; + + +Object3DUtil.clearCache = function (object3D) { + Object3DUtil.traverse( + object3D, + function (node) { + delete node.cached; + return true; + }); +}; + +Object3DUtil.dispose = function(parentObject) { + parentObject.traverse(function (node) { + if (node instanceof THREE.Mesh) { + if (node.geometry) { + node.geometry.dispose(); + } + + if (node.material) { + + if (node.material instanceof THREE.MultiMaterial) { + node.material.materials.forEach(function (mtrl, idx) { + if (mtrl.map) mtrl.map.dispose(); + if (mtrl.lightMap) mtrl.lightMap.dispose(); + if (mtrl.bumpMap) mtrl.bumpMap.dispose(); + if (mtrl.normalMap) mtrl.normalMap.dispose(); + if (mtrl.specularMap) mtrl.specularMap.dispose(); + if (mtrl.envMap) mtrl.envMap.dispose(); + + mtrl.dispose(); // disposes any programs associated with the material + }); + } + else { + if (node.material.map) node.material.map.dispose(); + if (node.material.lightMap) node.material.lightMap.dispose(); + if (node.material.bumpMap) node.material.bumpMap.dispose(); + if (node.material.normalMap) node.material.normalMap.dispose(); + if (node.material.specularMap) node.material.specularMap.dispose(); + if (node.material.envMap) node.material.envMap.dispose(); + + node.material.dispose(); // disposes any programs associated with the material + } + } + } + }); +}; + +Object3DUtil.getModelInstance = function (object3D, searchAncestors) { + if (!object3D) return; + if (object3D.metadata && object3D.metadata.modelInstance) { + return object3D.metadata.modelInstance; + } + if (searchAncestors) { + var modelInstance = undefined; + Object3DUtil.traverseAncestors(object3D, function(obj) { + if (obj.metadata && obj.metadata.modelInstance) { + modelInstance = obj.metadata.modelInstance; + return false; + } else { + return true; + } + }); + return modelInstance; + } +}; + +Object3DUtil.traverseAncestors = function(object3D, callback) { + var parent = object3D.parent; + if ( parent != null ) { + var continueTraversal = callback( parent ); + if (continueTraversal) { + Object3DUtil.traverseAncestors(parent, callback); + } + } +}; + +// Find nodes that returns true for the given filter +Object3DUtil.findNodes = function(object3D, filter) { + var nodes = []; + Object3DUtil.traverse( + object3D, + function (node) { + if (filter(node)) { + nodes.push(node); + } + return true; + }); + return nodes; +}; + +Object3DUtil.removeNodes = function(node, filter) { + var matches = Object3DUtil.findNodes(node, filter); + for (var i = 0; i < matches.length; i++) { + var match = matches[i]; + match.parent.remove(match); + } + return matches; +}; + + +// Return object with minimum distance +Object3DUtil.getMinDistanceToObjectBBoxes = function(object3Ds, points, opt) { + var minDist = Infinity; + var index = null; + //console.log('length', object3Ds.length); + for (var i = 0; i < object3Ds.length; i++) { + var o = object3Ds[i]; + var bbox = Object3DUtil.getBoundingBox(o); + var dists = _.map(points, function(point) { return bbox.distanceToPoint(point, opt); }); + //console.log('dists', dists); + var dist = _.min(dists); + if (dist < minDist) { + //console.log('dist ', dist, o.userData); + minDist = dist; + index = i; + } + } + return { dist: minDist, index: i }; +}; + +Object3DUtil.removeAllChildren = function (object3D) { + while (object3D.children.length > 0) { + object3D.remove(object3D.children[0]); + } +}; + +Object3DUtil.setVisible = function (object, visible, recursive) { + if (object) { + object.visible = visible; + if (recursive) { + object.traverse(function(x) { + x.visible = visible; + }); + } + } +}; + +Object3DUtil.setChildrenVisible = function (object, isVisible) { + if (object) { + for (var i = 0; i < object.children.length; i++) { + var c = object.children[i]; + c.visible = isVisible(c); + } + } +}; + +Object3DUtil.setState = function (object, field, value, filter) { + if (object) { + Object3DUtil.traverse(object, function (node) { + if (!filter || filter(node)) { + node.userData[field] = value; + return true; + } + }); + } +}; + +Object3DUtil.getMeshList = function (object, recursive, meshes) { + meshes = meshes || []; + Object3DUtil.traverseMeshes( + object, + !recursive, + function (mesh) { + meshes.push(mesh); + } + ); + return meshes; +}; + +Object3DUtil.getVisibleMeshList = function (object, recursive, meshes) { + meshes = meshes || []; + Object3DUtil.traverseVisibleMeshes( + object, + !recursive, + function (mesh) { + meshes.push(mesh); + } + ); + return meshes; +}; + +// Be careful when using function below.... +Object3DUtil.getMeshes = function (object, recursive) { + var meshes = { + list: [], + map: {} + }; + Object3DUtil.traverseMeshes( + object, + !recursive, + function (mesh) { + if (mesh.userData && mesh.userData.hasOwnProperty('index')) { + meshes.list[mesh.userData.index] = mesh; + } else { + meshes.list.push(mesh); + } + meshes.map[mesh.id] = mesh; + } + ); + return meshes; +}; + +Object3DUtil.findClosestBBFaceByOutNormal = function (outNorm, threshold) { + if (threshold === undefined) { + threshold = 0.99; + } + for (var i = 0; i < Object3DUtil.OutNormals.length; i++) { + var norm = Object3DUtil.OutNormals[i]; + if (outNorm.dot(norm) >= threshold) { + return i; + } + } + return -1; +}; + +Object3DUtil.findClosestBBFaceByInNormal = function (inNorm, threshold) { + if (threshold === undefined) { + threshold = 0.99; + } + for (var i = 0; i < Object3DUtil.InNormals.length; i++) { + var norm = Object3DUtil.InNormals[i]; + if (inNorm.dot(norm) >= threshold) { + return i; + } + } + return -1; +}; + +// segments is a array of segments +// Each segment can have one of the following formats: +// a. array of meshTri: [...] +// b. object with field meshTri: { meshTri: array of meshTri } +// c. object with meshTri fields: {meshIndex: x, triIndex: [...] } +// Each meshTri has following fields: {meshIndex: x, triIndex: [...] } +Object3DUtil.remeshObject = function (object, segments, material) { + // Get remesh of the object + var origMeshes = Object3DUtil.getMeshes(object); + origMeshes = origMeshes.list; + + // Clone the relevant mesh + var remeshedObj = new THREE.Object3D(); + remeshedObj.name = object.name + '-remeshed'; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + var meshTris = [segment]; // Default to case c) + if (segment.hasOwnProperty('meshTri')) { + // Case b + meshTris = segment['meshTri']; + } else if (segment.length) { + // Case a + meshTris = segment; + } + var componentMeshes = []; + + for (var j = 0; j < meshTris.length; j++) { + var meshTri = meshTris[j]; + var meshIndex = meshTri.meshIndex; + var origMesh = (meshTri.mesh) ? meshTri.mesh : origMeshes[meshIndex]; + + var componentMesh; + if (meshTri.triIndex) { + // Remesh with specific triangles + componentMesh = GeometryUtil.extractMesh(origMesh, meshTri.triIndex); + } else { + // Just my mesh + componentMesh = origMesh.clone(); + } + + // Get world transform from our parent + // TODO: if there is a scene transform, it needs to be undone by the viewer... + var parent = origMesh.parent; + if (parent) { + componentMesh.applyMatrix(parent.matrixWorld); + componentMesh.matrixWorldNeedsUpdate = true; + } + if (material) { + componentMesh.material = material; + } + componentMeshes.push(componentMesh); + } + + var myMesh = GeometryUtil.mergeMeshes(componentMeshes); + myMesh.name = (segment.id != undefined)? segment.id : object.name + '-remeshed-' + i; + myMesh.userData = { + id: (segment.id != undefined)? segment.id : 'mesh' + i, + index: i + }; + remeshedObj.add(myMesh); + } + // Clear out any __bufferGeometry + for (var i = 0; i < origMeshes.length; i++) { + delete origMeshes[i].__bufferGeometry; + } + return remeshedObj; +}; + +Object3DUtil.assignMultiMaterialToSegments = function (object, segments, createMaterialFn) { + if (!createMaterialFn) { + createMaterialFn = function(index, segment) { + var material = Object3DUtil.getSimpleFalseColorMaterial(index); + return material; + }; + } + // Get remesh of the object + var origMeshes = Object3DUtil.getMeshes(object); + + // Clone the original meshes + var materials = []; + var multiMaterial = new THREE.MultiMaterial(materials); + var remeshedObj = new THREE.Object3D(); + remeshedObj.name = object.name + '-remeshed'; + var clonedMeshes = _.map(origMeshes.list, function(x) { + x.updateMatrixWorld(); + var cloned = x.clone(); + cloned.geometry = GeometryUtil.toGeometry(cloned.geometry); + cloned.material = multiMaterial; + cloned.matrix.copy(x.matrixWorld); + cloned.matrixWorldNeedsUpdate = true; + remeshedObj.add(cloned); + return cloned; + }); + + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + materials[i] = createMaterialFn(i, segment); + var meshTris = [segment]; // Default to case c) + if (segment.hasOwnProperty('meshTri')) { + // Case b + meshTris = segment['meshTri']; + } else if (segment.length) { + // Case a + meshTris = segment; + } + + for (var j = 0; j < meshTris.length; j++) { + var meshTri = meshTris[j]; + var meshIndex = meshTri.meshIndex; + var clonedMesh = (meshTri.mesh) ? meshTri.mesh : clonedMeshes[meshIndex]; + + // TODO: Try to handle Buffer Geometry too + var faces = clonedMesh.geometry.faces; + if (meshTri.triIndex) { + var triIndices = meshTri.triIndex; + for (var ti in triIndices) { + if (triIndices.hasOwnProperty(ti)) { + faces[ti].materialIndex = i; + } + } + } else { + for (var k = 0; k < faces.length; k++) { + faces[k].materialIndex = i; + } + } + } + + } + return remeshedObj; +}; + +// segmentGroups is a array of segment groups +// Each segmentGroup is a object with id, label, objectId, obb, and segments (optional) +// vertToSegIndices is a mapping of vertex to segment index +Object3DUtil.remeshObjectUsingSegmentGroups = function (object, segmentGroups, vertToSegIndices, quiet) { + // Go over segment groups + var origMeshes = Object3DUtil.getMeshes(object); + // Assumes just one mesh + var origMesh = origMeshes.list[0]; + var segToTriIndices = GeometryUtil.segVertIndicesToSegTriIndices(origMesh, vertToSegIndices); + // Convert to buffered geometry since extractMeshVertIndices works faster with buffered geometry + var origMeshBuffered = origMesh.clone(); + origMeshBuffered.geometry = GeometryUtil.toBufferGeometry(origMesh.geometry); + var remeshedObj = new THREE.Object3D(); + remeshedObj.name = object.name + '-remeshed'; + var noIndices = []; + for (var i = 0; i < segmentGroups.length; i++) { + var segGroup = segmentGroups[i]; + if (segGroup.segments && segGroup.segments.length > 0) { + //console.time('triIndices'); + var segs = segGroup.segments; + var triIndices = []; + for (var si = 0; si < segs.length; si++) { + var vis = segToTriIndices[segs[si]]; + if (vis) { + for (var j = 0; j < vis.length; j++) { + triIndices.push(vis[j]); + } + //Array.prototype.push.apply(triIndices, vis); + } else { + noIndices.push(segs[si]); + } + } + //console.time('triIndicesUniq'); + triIndices = _.uniq(triIndices); + //console.timeEnd('triIndicesUniq'); + //console.timeEnd('triIndices'); + var myMesh = GeometryUtil.extractMesh(origMeshBuffered, triIndices); + var parent = origMesh.parent; + if (parent) { + myMesh.applyMatrix(parent.matrixWorld); + myMesh.matrixWorldNeedsUpdate = true; + } + myMesh.name = object.name + '-remeshed-' + i; + myMesh.userData = segGroup; + segGroup['index'] = i; + remeshedObj.add(myMesh); + } + } + if (!quiet && noIndices.length > 0) { + console.error('No indices for ' + noIndices.length + ' segments', noIndices); + } + return remeshedObj; +}; + +// vertToSegIndices is a mapping of vertex index to a mapped segment index, negative segment indices are not used +Object3DUtil.copyAndRecolorVertices = function (object, vertToSegIndices, color) { + // Go over segment groups + var meshes = Object3DUtil.getMeshes(object); + // Assumes just one mesh + var mesh = meshes.list[0]; + var geometry = mesh.geometry.clone(); + var gray = new THREE.Color(0.5, 0.5, 0.5); + var colors = []; + function getColor(vi) { + var si = vertToSegIndices[vi]; + if (si == undefined) { + return gray; + } else { + if (!colors[si]) { + if (color) { + colors[si] = color.clone(); + } else { + colors[si] = (si >= 0) ? Object3DUtil.createColor(Math.abs(si)) : gray.clone(); + } + } + return colors[si]; + } + } + if (geometry.faces) { + // Maintain array of colors for vertex (for easy update) + // TODO: Do we need this in memory? + // geometry.colors = []; + // var nVertices = GeometryUtil.getGeometryVertexCount(geometry); + // for (var i = 0; i < nVertices; i++) { + // geometry.colors.push(getColor(i)); + // } + // Update face vertex + for (var i = 0; i < geometry.faces.length; i++) { + geometry.faces[i].vertexColors = [ + getColor(geometry.faces[i].a), + getColor(geometry.faces[i].b), + getColor(geometry.faces[i].c) + ]; + } + geometry.colorsNeedUpdate = true; + geometry.elementsNeedUpdate = true; + } else { + var attributes = geometry.attributes; + if ( attributes.position ) { + var positions = attributes.position.array; + if (attributes.color === undefined) { + geometry.addAttribute('color', new THREE.BufferAttribute(new Float32Array(positions.length), 3)); + } + // Update vertex colors + var vcolors = attributes.color.array; + for (var i = 0; i < vcolors.length; i+=3) { + var vi = i / 3; + var c = getColor(vi); + vcolors[i] = c.r; + vcolors[i+1] = c.g; + vcolors[i+2] = c.b; + } + } + } + geometry.computeFaceNormals(); + geometry.computeVertexNormals(); + var material = new THREE.MeshPhongMaterial({ vertexColors: THREE.VertexColors }); + var recolored = new THREE.Mesh(geometry, material); + recolored.name = object.name + '-recolored'; + recolored.userData.segColors = colors; + Object3DUtil.setMatrix(recolored, mesh.matrixWorld); + return recolored; +}; + +Object3DUtil.getMeshMaterials = function(object3D) { + // Returns a list of materials with mesh and material index; + var materials = []; + Object3DUtil.traverseMeshes(object3D, true, function (node) { + if (node instanceof THREE.Mesh && node.material) { + if (node.material instanceof THREE.MultiMaterial) { + // Actual material definition is embedded in geometry... + var mats = node.material.materials; + for (var j = 0; j < mats.length; j++) { + var m = mats[j]; + materials.push({ mesh: node, material: m, index: j }); + } + } else { + materials.push({ mesh: node, material: node.material, index: node.material.index }); + } + } + }); + return materials; +}; + +Object3DUtil.getMaterials = function (object3D) { + // each entry of materials holds + // name: name of material + // material: actual material + // meshes: meshes with which the material is associated + var materials = {}; + var count = 0; + var allMeshes = []; + var allMeshesWithTexture = []; + var allMaterials = []; + var allMaterialsWithTexture = []; + + function addMaterial(mesh, material) { + var id = material.id; + var name = material.name; + if (id === undefined) { + //noinspection JSUnresolvedVariable + if (THREE.MaterialCount === undefined) { + THREE.MaterialCount = 0; + } + id = THREE.MaterialCount++; + material.id = id; + console.log('Material without id (assigned id ' + id + ')'); + } + if (name === undefined) { + name = 'mat' + count; + material.name = name; + console.log('Material ' + id + ' without name (assigned name ' + name + ')'); + } + var old = materials[id]; + if (old) { + old.meshes.push(mesh); + } else { + materials[id] = { + id: id, + name: name, + material: material, + meshes: [mesh], + type: 'material' + }; + } + allMaterials.push(id); + allMeshes.push(mesh); + if (material.map) { + allMeshesWithTexture.push(mesh); + allMaterialsWithTexture.push(id); + } + count++; + } + + object3D.traverse(function (node) { + if (node instanceof THREE.Mesh && node.material) { + if (node.material instanceof THREE.MultiMaterial) { + // Actual material definition is embedded in geometry... + var meshFaceMaterial = node.material; + var meshFaces = []; + // Break down materials into individual faces... + // TODO: Same as geometryGroups/geometryGroupsList in geometry? + // If so, just use materialIndex instead of tracking the faceIndices... + if (node.geometry.faces) { + for (var j = 0; j < node.geometry.faces.length; j++) { + var face = node.geometry.faces[j]; + var materialIndex = face.materialIndex; + if (!meshFaces[materialIndex]) { + meshFaces[materialIndex] = [j]; + } else { + meshFaces[materialIndex].push(j); + } + } + } else { + var nFaces = GeometryUtil.getGeometryFaceCount(node.geometry); + for (var j = 0; j < nFaces; j++) { + var group = _.find(node.geometry.groups, function (g) { + return (j >= g.start) && (j < g.start + g.count); + }); + var materialIndex = group? group.materialIndex : 0; + if (!meshFaces[materialIndex]) { + meshFaces[materialIndex] = [j]; + } else { + meshFaces[materialIndex].push(j); + } + } + } + for (var i = 0; i < meshFaceMaterial.materials.length; i++) { + if (meshFaces.length > 0) { + addMaterial({mesh: node, faceIndices: meshFaces[i], materialIndex: i}, meshFaceMaterial.materials[i]); + } + } + } else { + addMaterial(node, node.material); + } + } + }); + materials['all'] = { + id: 'all', name: 'All', type: 'material_set', + meshes: allMeshes, materials: allMaterials + }; + materials['textured'] = { + id: 'textured', name: 'Textured', type: 'material_set', + meshes: allMeshesWithTexture, materials: allMaterialsWithTexture + }; + return materials; +}; + +Object3DUtil.getSceneGraphPath = function (node, parentRoot) { + // Follow up to parents + var path = []; + var parent = node; + var child; + while (parent && parent !== parentRoot) { + var name = parent.id; + if (parent['userData']) { + var userdata = parent.userData; + if (userdata.hasOwnProperty('id')) { + name = userdata['id']; + } + } + if (child) { + name = name + '[' + parent.children.indexOf(child) + ']'; + } + path.push(name); + // Go up + child = parent; + parent = parent.parent; + } + path.reverse(); + if (path.length > 0) { + return '/' + path.join('/'); + } else { + return ''; + } +}; + +Object3DUtil.getNodeFromSceneGraphPath = function (parentRoot, path) { + if (typeof (path) === 'string') { + path = path.split('/'); + } + if (path[0] === '') { + path.shift(); + } + var parent = parentRoot.children[0]; + var regex = /^(.*)\[(\d+)\]$/; + for (var i = 0; i < path.length; i++) { + var userdata = parent.userData; + var name = ''; + if (userdata.hasOwnProperty('id')) { + name = userdata['id']; + } + var matched = regex.exec(path[i]); + if (matched) { + var ci = matched[2]; + var child = parent.children[ci]; + if (!child) { + console.warn('Cannot find child ' + path[i]); + break; + } + if (matched[1] !== name) { + console.warn('Name does not match: expected ' + matched[1] + ', actual ' + name); + } + parent = child; + } else { + if (i < path.length - 1) { + console.warn('Exiting search for ' + path); + } + break; + } + } + if (i >= path.length - 1) { + return parent; + } else { + return null; + } +}; + +Object3DUtil.findMaterials = function(object3D, nonrecursive, materialFilter) { + var materials = []; + Object3DUtil.traverseMeshes(object3D, nonrecursive, function(mesh) { + //console.log('traverse', mesh); + var material = mesh.material; + if (material instanceof THREE.MultiMaterial) { + for (var i = 0; i < material.materials.length; i++) { + var mat = material.materials[i]; + if (materialFilter(mat)) { + materials.push({ + mesh: mesh, + material: mat, + setMaterial: function(index, newMat) { + material.materials[index] = newMat; + }.bind(mesh, i) + }); + } + } + } else { + if (materialFilter(material)) { + materials.push({ + mesh: mesh, + material: material, + setMaterial: function(newMat) { + mesh.material = newMat; + }.bind(mesh) + }); + } + } + }); + return materials; +}; + +Object3DUtil.addMirrors = function(object3D, opts) { + var mirrors = []; + Object3DUtil.traverseMeshes(object3D, false, function(mesh) { + //console.log('Check for mirror material', object3D); + // Find materials with mirror + var mirrorMatName = opts.mirrorMaterialName || 'mirror'; + var mirrorMats = Object3DUtil.findMaterials(mesh, true, opts.filterMaterial || function (m) { + //console.log('check ' + m.name, m); + return m.name === mirrorMatName && !m.map; + }); + + // Check if it has material with name video + var hasMirror = mirrorMats.length > 0; + if (hasMirror) { + var Mirror = require('capabilities/Mirror'); + _.each(mirrorMats, function(mat) { + mesh.mirror = new Mirror(_.defaults({object3D: mesh, mirrorMaterials: [mat], color: mat.material.color}, opts)); + mirrors.push(mesh.mirror); + }); + } + }); + return mirrors; +}; + +Object3DUtil.addVideoPlayer = function(object3D, opts) { + //console.log('Check for video capability', object3D); + // Find materials with video + var videoMatName = opts.videoMaterialName || 'video'; + var videoMats = Object3DUtil.findMaterials(object3D, true, opts.filterMaterial || function(m) { + //console.log('check ' + m.name, m); + return m.name === videoMatName; + }); + + // Check if it has material with name video + var hasVideo = videoMats.length > 0; + if (hasVideo) { + var VideoPlayer = require('capabilities/VideoPlayer'); + object3D.videoPlayer = new VideoPlayer({ + object3D: object3D, + videoMaterials: videoMats, + assetManager: opts.assetManager + }); + } + return object3D.videoPlayer; +}; + +// TODO(MS): Experimental function to push transform to leaf-like nodes +Object3DUtil.pushWorldTransformToMeshes = function (object3D) { + Object3DUtil.traverse(object3D, function (node) { + if (node instanceof THREE.Mesh || node instanceof THREE.Light) { + Object3DUtil.setMatrix(node, node.matrixWorld); + return false; + } + return true; + }, function (node) { + var isStop = node instanceof THREE.Mesh || node instanceof THREE.Light; + if (!isStop) { + Object3DUtil.setMatrix(node, new THREE.Matrix4()); + } + }); +}; + +Object3DUtil.findLights = function(object3D) { + return Object3DUtil.findNodes(object3D, function (node) { + return node instanceof THREE.Light; + }); +}; + +Object3DUtil.setLights = function (object3D, flag) { + var lights = Object3DUtil.findLights(object3D); + for (var i = 0; i < lights.length; ++i) { + var l = lights[i]; + l.userData.isOn = flag; + if (l.userData.isOn) { + l.intensity = l.userData.intensity; + } else { + l.intensity = 0; + } + } + return lights.length; +}; + +Object3DUtil.setMaterialState = function (object3D, matOverrides, flag) { + if (!matOverrides) { return; } + // overrides for emissive surfaces and shade transparency + function setMatState(meshMaterial, matIndex) { + //console.log(matIndex); + if (matOverrides.emissive && matOverrides.emissive.indexOf(matIndex) >= 0) { + //console.log('emissive', matIndex); + if (flag) { + meshMaterial.emissive.set(0xFFFF00); + meshMaterial.emissiveIntensity = 5; + } else { + meshMaterial.emissive.set(0x000000); + meshMaterial.emissiveIntensity = 0; + } + } + if (matOverrides.shade && matOverrides.shade.indexOf(matIndex) >= 0) { + if (!meshMaterial.defaults) { + meshMaterial.defaults = { + opacity: meshMaterial.opacity, transparent: meshMaterial.transparent + }; + } + if (flag) { + meshMaterial.transparent = true; + meshMaterial.opacity = 0.9; + } else { + meshMaterial.transparent = meshMaterial.defaults.transparent; + meshMaterial.opacity = meshMaterial.defaults.opacity; + } + } + } + + Object3DUtil.traverseMeshes(object3D, true, function (mesh) { + if (mesh.material instanceof THREE.MultiMaterial) { + for (var i = 0, l = mesh.material.materials.length; i < l; i++) { + var meshMaterial = mesh.material.materials[i]; + setMatState(meshMaterial, i); + } + } else if (mesh.material.index != undefined) { + setMatState(mesh.material, mesh.material.index); + } + }); +}; + +Object3DUtil.setCapability = function(object3D, name, flag) { + if (!object3D.userData.capabilities) { + object3D.userData.capabilities = {}; + } + object3D.userData.capabilities[name] = flag; +}; + +Object3DUtil.getCapability = function(object3D, name, defaultValue) { + var v; + if (object3D.userData.capabilities) { + v = object3D.userData.capabilities[name]; + if (v != undefined) { + return v; + } + } + if (defaultValue != undefined) { + if (typeof defaultValue === "function") { + v = defaultValue(); + } else { + v = defaultValue; + } + Object3DUtil.setCapability(object3D, name, v || false); + return v; + } +}; + +Object3DUtil.getCapabilities = function(object3D) { + return object3D.userData.capabilities; +}; + +Object3DUtil.addSphere = function (scene, centerTo, size, materialOrColor) { + size = size || 10; + var sphereGeo = new THREE.SphereGeometry(size); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(sphereGeo, material); + mesh.position.copy(centerTo); + scene.add(mesh); +}; + +Object3DUtil.setCylinderDirection = function (obj, dir) { + // Assumes dir is normalized + if (dir.y > 0.99999) { + obj.quaternion.set(0, 0, 0, 1); + } else if (dir.y < -0.99999) { + obj.quaternion.set(1, 0, 0, 0); + } else { + var axis = new THREE.Vector3(); + axis.set(dir.z, 0, -dir.x).normalize(); + var radians = Math.acos(dir.y); + obj.quaternion.setFromAxisAngle(axis, radians); + } +}; + +Object3DUtil.makeColumn = function (basePoint, columnDir, height, width, materialOrColor) { + width = width || 10; + var cylinderGeo = new THREE.CylinderGeometry(width, width, height); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(cylinderGeo, material); + mesh.name = 'Column'; + var centerTo = basePoint.clone().add(columnDir.clone().multiplyScalar(height / 2)); + Object3DUtil.setCylinderDirection(mesh, columnDir); + mesh.position.copy(centerTo); + return mesh; +}; + +Object3DUtil.addColumn = function (scene, basePoint, columnDir, height, width, materialOrColor) { + var column = Object3DUtil.makeColumn(basePoint, columnDir, height, width, materialOrColor); + scene.add(column); +}; + +Object3DUtil.makeCylinder = function (start, end, width, materialOrColor) { + var columnDir = end.clone().sub(start).normalize(); + var height = start.distanceTo(end); + var cylinderGeo = new THREE.CylinderGeometry(width, width, height); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(cylinderGeo, material); + mesh.name = 'Cylinder'; + var centerTo = start.clone().add(end).multiplyScalar(0.5); + Object3DUtil.setCylinderDirection(mesh, columnDir); + mesh.position.copy(centerTo); + return mesh; +}; + +Object3DUtil.makeBall = function (origin, radius, materialOrColor) { + var ballGeo = new THREE.SphereBufferGeometry(radius); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(ballGeo, material); + mesh.name = 'Ball'; + mesh.position.copy(origin); + return mesh; +}; + +Object3DUtil.makeBalls = function (points, radius, materialOrColor, name, group) { + name = name || 'Ball'; + group = group || new THREE.Group(); + for (var i = 0; i < points.length; i++) { + var ball = Object3DUtil.makeBall(points[i], radius, materialOrColor); + ball.name = name + i; + group.add(ball); + } + return group; +}; + +Object3DUtil.makeBoxFromToOrientation = function (from, to, orientation, size, materialOrColor) { + var dist = to.distanceTo(from); // * 0.5; + var center = from.clone().add(to).multiplyScalar(0.5); + + var boxGeo = new THREE.BoxGeometry(dist, size, size); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(boxGeo, material); + mesh.setRotationFromQuaternion(orientation); + mesh.position.copy(center); + return mesh; +}; + +Object3DUtil.makeWall = function (baseStart, baseEnd, wallUpDir, height, depth, materialOrColor) { + depth = depth || 10; + var width = baseEnd.distanceTo(baseStart); + // x = width, y = height, z = depth + // Build box geo + var boxGeo = new THREE.BoxGeometry(width, height, depth); + var material = Object3DUtil.getMaterial(materialOrColor); + var mesh = new THREE.Mesh(boxGeo, material); + mesh.name = 'Wall'; + + // Take start --> end to be right, front to be normal + var startToEnd = baseEnd.clone().sub(baseStart).normalize(); + var wallFrontDir = new THREE.Vector3(); + wallFrontDir.crossVectors(startToEnd, wallUpDir).normalize(); + var alignMatrix = Object3DUtil.getAlignmentMatrix(new THREE.Vector3(0,1,0), new THREE.Vector3(0,0,1), + wallUpDir, wallFrontDir); + + var centerTo = baseStart.clone().add(baseEnd).multiplyScalar(0.5); + centerTo.add(wallUpDir.clone().multiplyScalar(height / 2)); + alignMatrix.setPosition(centerTo); + + Object3DUtil.setMatrix(mesh, alignMatrix); + return mesh; +}; + +// Make geometry for one pieces of a wall +Object3DUtil.__makeWallWithHolesGeometry = function(opts) { + var holes = opts.holes; + var height = opts.height; + var depth = opts.depth; + var width = opts.width; + + var initialHeight = opts.initialHeight; + var initialWidth = opts.initialWidth; + + var wdelta = (width - initialWidth) / 2; + + // Assume holes are disjoint + // Split holes into outside (on border) and inside holes (inside wall) + var outsideHoles = []; + var leftHoles = [], rightHoles = [], topHoles = [], bottomHoles = []; + var insideHoles = []; + if (holes) { + for (var iHole = 0; iHole < holes.length; iHole++) { + var hole = holes[iHole]; + if (hole.min.x <= 0 && hole.max.x < initialWidth) { + leftHoles.push(hole); + outsideHoles.push(hole); + } else if (hole.min.x > 0 && hole.max.x >= initialWidth) { + rightHoles.push(hole); + outsideHoles.push(hole); + } else if (hole.min.y <= 0 && hole.max.y < initialHeight) { + bottomHoles.push(hole); + outsideHoles.push(hole); + } else if (hole.min.y > 0 && hole.max.y >= initialHeight) { + topHoles.push(hole); + outsideHoles.push(hole); + } else { + insideHoles.push(hole); + } + } + } + + var wallShape = new THREE.Shape(); + if (outsideHoles.length > 0) { + //console.log('processing outside holes', bottomHoles, rightHoles, topHoles, leftHoles); + bottomHoles = _.sortBy(bottomHoles, function(hole) { return hole.min.x; }); + rightHoles = _.sortBy(rightHoles, function(hole) { return hole.max.y; }); + topHoles = _.sortBy(topHoles, function(hole) { return -hole.max.x; }); + leftHoles = _.sortBy(leftHoles, function(hole) { return -hole.max.y; }); + + // Start at lower left + wallShape.moveTo(0, 0); + + // Process bottom holes + for (var i = 0; i < bottomHoles.length; i++) { + var hole = bottomHoles[i]; + wallShape.lineTo(wdelta + hole.min.x, 0); + wallShape.lineTo(wdelta + hole.min.x, hole.max.y); + wallShape.lineTo(wdelta + hole.max.x, hole.max.y); + wallShape.lineTo(wdelta + hole.max.x, 0); + } + wallShape.lineTo(width, 0); + + // Process right holes + for (var i = 0; i < rightHoles.length; i++) { + var hole = rightHoles[i]; + wallShape.lineTo(width, hole.min.y); + wallShape.lineTo(wdelta + hole.min.x, hole.min.y); + wallShape.lineTo(wdelta + hole.min.x, hole.max.y); + wallShape.lineTo(width, hole.max.y); + } + // Go to top + if (!hole || hole.max.y < height) { + wallShape.lineTo(width, height); + } + + // Process top holes + for (var i = 0; i < topHoles.length; i++) { + var hole = topHoles[i]; + wallShape.lineTo(wdelta + hole.max.x, height); + wallShape.lineTo(wdelta + hole.max.x, hole.min.y); + wallShape.lineTo(wdelta + hole.min.x, hole.min.y); + wallShape.lineTo(wdelta + hole.min.x, height); + } + wallShape.lineTo(0, height); + + // Process left holes + for (var i = 0; i < leftHoles.length; i++) { + var hole = leftHoles[i]; + wallShape.lineTo(0, hole.max.y); + wallShape.lineTo(wdelta + hole.max.x, hole.max.y); + wallShape.lineTo(wdelta + hole.max.x, hole.min.y); + wallShape.lineTo(0, hole.min.y); + } + + if (!hole || hole.min.y > 0) { + wallShape.lineTo(0, 0); + } + } else { + wallShape.moveTo(0, 0); + wallShape.lineTo(width, 0); + wallShape.lineTo(width, height); + wallShape.lineTo(0, height); + wallShape.lineTo(0, 0); + } + + if (insideHoles.length) { + for (var iHole = 0; iHole < insideHoles.length; iHole++) { + var hole = insideHoles[iHole]; + var holePath = new THREE.Path(); + // Make sure holes are inside wall... + var minx = Math.max(wdelta + hole.min.x, 0.000); + var maxx = Math.min(wdelta + hole.max.x, width - 0.000); + var miny = Math.max(hole.min.y, 0.000); + var maxy = Math.min(hole.max.y, height - 0.000); + + holePath.moveTo(minx, miny); + holePath.lineTo(maxx, miny); + holePath.lineTo(maxx, maxy); + holePath.lineTo(minx, maxy); + holePath.lineTo(minx, miny); + wallShape.holes.push(holePath); + } + } + + var extrudeSettings = { amount: depth / 2, bevelEnabled: false }; + var geo = new THREE.ExtrudeGeometry(wallShape, extrudeSettings); + return geo; +}; + +Object3DUtil.makeWallWithHoles = function (baseStart, baseEnd, wallUpDir, wallHeight, extraHeight, + depth, holes, materials) { + depth = depth || 10; + var height = wallHeight + extraHeight; + var wallDir = baseEnd.clone().sub(baseStart).normalize(); + var wallFrontDir = new THREE.Vector3(); + wallFrontDir.crossVectors(wallDir, wallUpDir).normalize(); + var wallEndOffset = wallDir.clone().multiplyScalar(depth / 2 - depth / 10); + + var p0 = baseStart.clone().sub(wallEndOffset); + var p1 = baseEnd.clone().add(wallEndOffset); + var width = p1.distanceTo(p0); + + // Account for slight difference in original width and extended width + var initialWidth = baseStart.distanceTo(baseEnd); + + var geo = Object3DUtil.__makeWallWithHolesGeometry({ + holes: holes, + height: height, + depth: depth, + width: width, + initialWidth: initialWidth, + initialHeight: wallHeight + }); + + var materialBetween = Object3DUtil.getBasicMaterial('gray'); + var materialIn = new THREE.MultiMaterial([materials[0], materialBetween]); + var materialOut = new THREE.MultiMaterial([materials[1], materialBetween]); + var meshIn = new THREE.Mesh(geo, materialIn); + var meshOut = new THREE.Mesh(geo, materialOut); + meshIn.name = 'WallInside'; + meshOut.name = 'WallOutside'; + + var alignMatrix = Object3DUtil.getAlignmentMatrix( + new THREE.Vector3(0,1,0), new THREE.Vector3(0,0,1), + wallUpDir, wallFrontDir); + alignMatrix.setPosition(p0); + Object3DUtil.setMatrix(meshIn, alignMatrix); + + var offset = wallFrontDir.clone().multiplyScalar(-depth / 2 + depth / 19); // NOTE: d/19 offset to avoid z-fighting + alignMatrix.setPosition(p0.clone().add(offset)); + Object3DUtil.setMatrix(meshOut, alignMatrix); + + var merged = new THREE.Object3D('DoubleSidedWall'); + merged.add(meshIn); + merged.add(meshOut); + merged.name = 'DoubleSidedWall'; + + return merged; +}; + +Object3DUtil.addWall = function (scene, baseStart, baseEnd, wallUpDir, height, depth, color) { + var wall = Object3DUtil.makeWall(baseStart, baseEnd, wallUpDir, height, depth, color); + scene.add(wall); +}; + +function mergeHoles(holeBBoxes) { + var mergedHoleIndices = []; + var finalHoleBoxes = []; + for (var i = 0; i < holeBBoxes.length; i++) { + if (mergedHoleIndices.indexOf(i) >= 0) { + continue; + } + var iHoleBBox = holeBBoxes[i]; + for (var j = i + 1; j < holeBBoxes.length; j++) { + var jHoleBBox = holeBBoxes[j]; + if (iHoleBBox.intersects(jHoleBBox)) { + mergedHoleIndices.push(j); + //console.log('Merging ' + jHoleBBox.toString() + ' to ' + iHoleBBox.toString()); + iHoleBBox.includeBBox(jHoleBBox); + //console.log('Got ' + iHoleBBox.toString()); + } + } + finalHoleBoxes.push(iHoleBBox); + } + return { holeBBoxes: finalHoleBoxes, mergedHoleIndices: mergedHoleIndices }; +} + +function repeatedMergeHoles(holeBBoxes) { + var m = mergeHoles(holeBBoxes); + while (m.mergedHoleIndices.length > 0) { + m = mergeHoles(m.holeBBoxes); + } + return m.holeBBoxes; +} + +Object3DUtil.mergeHoles = repeatedMergeHoles; + +Object3DUtil.getVisibleWidthHeight = function (camera, dist) { + var vFOV = camera.fov * Math.PI / 180; // convert vertical fov to radians + var height = 2 * Math.tan(vFOV / 2) * dist; // visible height + var width = height * camera.aspect; + return new THREE.Vector2(width, height); +}; + +Object3DUtil.makeCameraFrustum = function (origCamera) { + var camera = origCamera.clone(); + camera.near = 50; + camera.far = 100; + camera.updateProjectionMatrix(); + camera.updateMatrixWorld(); + return new THREE.CameraHelper(camera); +}; + +Object3DUtil.decomposeMatrix4 = function(matrix) { + var scale = new THREE.Vector3(); + var position = new THREE.Vector3(); + var orientation = new THREE.Quaternion(); + matrix.decompose(position, orientation, scale); + return { + position: position, + orientation: orientation, + scale: scale + }; +}; + +Object3DUtil.transferFaceAttributeToVertexColor = function(object3D, attribute, convertAttrFn) { + var color = new THREE.Color(); + Object3DUtil.colorVerticesUsingFaceAttribute(object3D, attribute, function(x) { + var v = convertAttrFn? convertAttrFn(x) : x; + color.setHex(v); + return color; + }); +}; + +Object3DUtil.colorVerticesUsingFaceAttribute = function(object3D, attribute, colorIndexOrFn) { + var colorFn = null; + if (colorIndexOrFn) { + if (_.isFunction(colorIndexOrFn)) { + colorFn = colorIndexOrFn; + } else { + colorFn = function(x) { return colorIndexOrFn[x]; }; + } + } + Object3DUtil.traverseMeshes(object3D, false, function(mesh) { + var geometry = mesh.geometry; + if (geometry.customFaceAttributes) { + var faceAttributes = geometry.customFaceAttributes[attribute]; + var vertexColorBuffer = geometry.attributes['color'].array; + console.log('got faceAttributes ' + attribute + ': ' + faceAttributes.length /*, _.min(faceAttributes), _.max(faceAttributes)*/); + GeometryUtil.forFaceVertexIndices(geometry, function(iFace, vertIndices) { + var v = faceAttributes[iFace]; + //console.log('got face attribute ' + v + ', vertices ' + JSON.stringify(vertIndices)); + var c = colorFn? colorFn(v) : v; + if (c == undefined) { + c = v || 0; + } + c = _.isInteger(c)? Object3DUtil.createColor(c) : Object3DUtil.getColor(c); + for (var i = 0; i < vertIndices.length; i++) { + var ci = vertIndices[i]*3; + vertexColorBuffer[ci] = c.r; + vertexColorBuffer[ci+1] = c.g; + vertexColorBuffer[ci+2] = c.b; + } + }); + } + }); +}; + +Object3DUtil.grayOutVertices = function(object3D, center, maxRadius, grayColor) { + Object3DUtil.traverseMeshes(object3D, false, function (mesh) { + GeometryUtil.grayOutVertices(mesh, center, maxRadius, grayColor); + }); +}; + +Object3DUtil.BlackMatParams = { + type: 'phong', + opacity: 1, + color: 0, + emissive: 0x000000, + specular: 0x000000, + side: THREE.DoubleSide, + name: 'black' +}; + +Object3DUtil.BlackMat = Object3DUtil.createMaterial(Object3DUtil.BlackMatParams); + +Object3DUtil.ClearMatParams = { + type: 'basic', + opacity: 0.1, + color: 0, + side: THREE.DoubleSide, + name: 'clear' +}; + +Object3DUtil.ClearMat = Object3DUtil.createMaterial(Object3DUtil.ClearMatParams); +Object3DUtil.ClearColor = new THREE.Color('gray'); + +Object3DUtil.TransparentMatParams = { + type: 'basic', + opacity: 0.4, + color: 0x111111, + side: THREE.DoubleSide, + name: 'transparent' +}; + +Object3DUtil.TransparentMat = Object3DUtil.createMaterial(Object3DUtil.TransparentMatParams); + +Object3DUtil.InvisibleMat = Object3DUtil.createMaterial({ + type: 'basic', + visible: false, + name: 'invisible' +}); + +// Exports +module.exports = Object3DUtil; diff --git a/client/js/OrbitControls.js b/client/js/OrbitControls.js new file mode 100644 index 0000000..bf662dd --- /dev/null +++ b/client/js/OrbitControls.js @@ -0,0 +1,1040 @@ +/** + * @author qiao / https://github.com/qiao + * @author mrdoob / http://mrdoob.com + * @author alteredq / http://alteredqualia.com/ + * @author WestLangley / http://github.com/WestLangley + * @author erich666 / http://erichaines.com + */ + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// +// Orbit - left mouse / touch: one finger move +// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish +// Pan - right mouse, or arrow keys / touch: three finger swipe + +THREE.OrbitControls = function ( object, domElement ) { + + this.object = object; + + this.domElement = ( domElement !== undefined ) ? domElement : document; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new THREE.Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = - Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.25; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 + + // Set to false to disable use of the keys + this.enableKeys = true; + + // The four arrow keys + this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; + + // Mouse buttons + this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // + // public methods + // + + this.getPolarAngle = function () { + + return spherical.phi; + + }; + + this.getAzimuthalAngle = function () { + + return spherical.theta; + + }; + + this.saveState = function () { + + scope.target0.copy( scope.target ); + scope.position0.copy( scope.object.position ); + scope.zoom0 = scope.object.zoom; + + }; + + this.reset = function () { + + scope.target.copy( scope.target0 ); + scope.object.position.copy( scope.position0 ); + scope.object.zoom = scope.zoom0; + + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( changeEvent ); + + scope.update(); + + state = STATE.NONE; + + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = function () { + + var offset = new THREE.Vector3(); + + // so camera.up is the orbit axis + var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); + var quatInverse = quat.clone().inverse(); + + var lastPosition = new THREE.Vector3(); + var lastQuaternion = new THREE.Quaternion(); + + return function update() { + + var position = scope.object.position; + + offset.copy( position ).sub( scope.target ); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion( quat ); + + // angle from z-axis around y-axis + spherical.setFromVector3( offset ); + + if ( scope.autoRotate && state === STATE.NONE ) { + + rotateLeft( getAutoRotationAngle() ); + + } + + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + + // restrict theta to be between desired limits + spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); + + // restrict phi to be between desired limits + spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); + + spherical.makeSafe(); + + + spherical.radius *= scale; + + // restrict radius to be between desired limits + spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); + + // move target to panned location + scope.target.add( panOffset ); + + offset.setFromSpherical( spherical ); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion( quatInverse ); + + position.copy( scope.target ).add( offset ); + + scope.object.lookAt( scope.target ); + + if ( scope.enableDamping === true ) { + + sphericalDelta.theta *= ( 1 - scope.dampingFactor ); + sphericalDelta.phi *= ( 1 - scope.dampingFactor ); + + } else { + + sphericalDelta.set( 0, 0, 0 ); + + } + + scale = 1; + panOffset.set( 0, 0, 0 ); + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( zoomChanged || + lastPosition.distanceToSquared( scope.object.position ) > EPS || + 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { + + scope.dispatchEvent( changeEvent ); + + lastPosition.copy( scope.object.position ); + lastQuaternion.copy( scope.object.quaternion ); + zoomChanged = false; + + return true; + + } + + return false; + + }; + + }(); + + this.dispose = function () { + + scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); + scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + window.removeEventListener( 'keydown', onKeyDown, false ); + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + + }; + + // + // internals + // + + var scope = this; + + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start' }; + var endEvent = { type: 'end' }; + + var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }; + + var state = STATE.NONE; + + var EPS = 0.000001; + + // current position in spherical coordinates + var spherical = new THREE.Spherical(); + var sphericalDelta = new THREE.Spherical(); + + var scale = 1; + var panOffset = new THREE.Vector3(); + var zoomChanged = false; + + var rotateStart = new THREE.Vector2(); + var rotateEnd = new THREE.Vector2(); + var rotateDelta = new THREE.Vector2(); + + var panStart = new THREE.Vector2(); + var panEnd = new THREE.Vector2(); + var panDelta = new THREE.Vector2(); + + var dollyStart = new THREE.Vector2(); + var dollyEnd = new THREE.Vector2(); + var dollyDelta = new THREE.Vector2(); + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function rotateLeft( angle ) { + + sphericalDelta.theta -= angle; + + } + + function rotateUp( angle ) { + + sphericalDelta.phi -= angle; + + } + + var panLeft = function () { + + var v = new THREE.Vector3(); + + return function panLeft( distance, objectMatrix ) { + + v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix + v.multiplyScalar( - distance ); + + panOffset.add( v ); + + }; + + }(); + + var panUp = function () { + + var v = new THREE.Vector3(); + + return function panUp( distance, objectMatrix ) { + + v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix + v.multiplyScalar( distance ); + + panOffset.add( v ); + + }; + + }(); + + // deltaX and deltaY are in pixels; right and down are positive + var pan = function () { + + var offset = new THREE.Vector3(); + + return function pan( deltaX, deltaY ) { + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + if ( scope.object instanceof THREE.PerspectiveCamera ) { + + // perspective + var position = scope.object.position; + offset.copy( position ).sub( scope.target ); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); + + // we actually don't use screenWidth, since perspective camera is fixed to screen height + panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); + panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); + + } else if ( scope.object instanceof THREE.OrthographicCamera ) { + + // orthographic + panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); + panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; + + } + + }; + + }(); + + function dollyIn( dollyScale ) { + + if ( scope.object instanceof THREE.PerspectiveCamera ) { + + scale /= dollyScale; + + } else if ( scope.object instanceof THREE.OrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + function dollyOut( dollyScale ) { + + if ( scope.object instanceof THREE.PerspectiveCamera ) { + + scale *= dollyScale; + + } else if ( scope.object instanceof THREE.OrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate( event ) { + + //console.log( 'handleMouseDownRotate' ); + + rotateStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownDolly( event ) { + + //console.log( 'handleMouseDownDolly' ); + + dollyStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownPan( event ) { + + //console.log( 'handleMouseDownPan' ); + + panStart.set( event.clientX, event.clientY ); + + } + + function handleMouseMoveRotate( event ) { + + //console.log( 'handleMouseMoveRotate' ); + + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + // rotating across whole screen goes 360 degrees around + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); + + // rotating up and down along whole screen attempts to go 360, but limited to 180 + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + + scope.update(); + + } + + function handleMouseMoveDolly( event ) { + + //console.log( 'handleMouseMoveDolly' ); + + dollyEnd.set( event.clientX, event.clientY ); + + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + dollyIn( getZoomScale() ); + + } else if ( dollyDelta.y < 0 ) { + + dollyOut( getZoomScale() ); + + } + + dollyStart.copy( dollyEnd ); + + scope.update(); + + } + + function handleMouseMovePan( event ) { + + //console.log( 'handleMouseMovePan' ); + + panEnd.set( event.clientX, event.clientY ); + + panDelta.subVectors( panEnd, panStart ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + scope.update(); + + } + + function handleMouseUp( event ) { + + // console.log( 'handleMouseUp' ); + + } + + function handleMouseWheel( event ) { + + // console.log( 'handleMouseWheel' ); + + if ( event.deltaY < 0 ) { + + dollyOut( getZoomScale() ); + + } else if ( event.deltaY > 0 ) { + + dollyIn( getZoomScale() ); + + } + + scope.update(); + + } + + function handleKeyDown( event ) { + + //console.log( 'handleKeyDown' ); + + switch ( event.keyCode ) { + + case scope.keys.UP: + pan( 0, scope.keyPanSpeed ); + scope.update(); + break; + + case scope.keys.BOTTOM: + pan( 0, - scope.keyPanSpeed ); + scope.update(); + break; + + case scope.keys.LEFT: + pan( scope.keyPanSpeed, 0 ); + scope.update(); + break; + + case scope.keys.RIGHT: + pan( - scope.keyPanSpeed, 0 ); + scope.update(); + break; + + } + + } + + function handleTouchStartRotate( event ) { + + //console.log( 'handleTouchStartRotate' ); + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } + + function handleTouchStartDolly( event ) { + + //console.log( 'handleTouchStartDolly' ); + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyStart.set( 0, distance ); + + } + + function handleTouchStartPan( event ) { + + //console.log( 'handleTouchStartPan' ); + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } + + function handleTouchMoveRotate( event ) { + + //console.log( 'handleTouchMoveRotate' ); + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + // rotating across whole screen goes 360 degrees around + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); + + // rotating up and down along whole screen attempts to go 360, but limited to 180 + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + + scope.update(); + + } + + function handleTouchMoveDolly( event ) { + + //console.log( 'handleTouchMoveDolly' ); + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyEnd.set( 0, distance ); + + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + dollyOut( getZoomScale() ); + + } else if ( dollyDelta.y < 0 ) { + + dollyIn( getZoomScale() ); + + } + + dollyStart.copy( dollyEnd ); + + scope.update(); + + } + + function handleTouchMovePan( event ) { + + //console.log( 'handleTouchMovePan' ); + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + panDelta.subVectors( panEnd, panStart ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + scope.update(); + + } + + function handleTouchEnd( event ) { + + //console.log( 'handleTouchEnd' ); + + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onMouseDown( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( event.button ) { + + case scope.mouseButtons.ORBIT: + + if ( scope.enableRotate === false ) return; + + handleMouseDownRotate( event ); + + state = STATE.ROTATE; + + break; + + case scope.mouseButtons.ZOOM: + + if ( scope.enableZoom === false ) return; + + handleMouseDownDolly( event ); + + state = STATE.DOLLY; + + break; + + case scope.mouseButtons.PAN: + + if ( scope.enablePan === false ) return; + + handleMouseDownPan( event ); + + state = STATE.PAN; + + break; + + } + + if ( state !== STATE.NONE ) { + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( startEvent ); + + } + + } + + function onMouseMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( state ) { + + case STATE.ROTATE: + + if ( scope.enableRotate === false ) return; + + handleMouseMoveRotate( event ); + + break; + + case STATE.DOLLY: + + if ( scope.enableZoom === false ) return; + + handleMouseMoveDolly( event ); + + break; + + case STATE.PAN: + + if ( scope.enablePan === false ) return; + + handleMouseMovePan( event ); + + break; + + } + + } + + function onMouseUp( event ) { + + if ( scope.enabled === false ) return; + + handleMouseUp( event ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + + if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; + + event.preventDefault(); + event.stopPropagation(); + + handleMouseWheel( event ); + + scope.dispatchEvent( startEvent ); // not sure why these are here... + scope.dispatchEvent( endEvent ); + + } + + function onKeyDown( event ) { + + if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; + + handleKeyDown( event ); + + } + + function onTouchStart( event ) { + + if ( scope.enabled === false ) return; + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + + if ( scope.enableRotate === false ) return; + + handleTouchStartRotate( event ); + + state = STATE.TOUCH_ROTATE; + + break; + + case 2: // two-fingered touch: dolly + + if ( scope.enableZoom === false ) return; + + handleTouchStartDolly( event ); + + state = STATE.TOUCH_DOLLY; + + break; + + case 3: // three-fingered touch: pan + + if ( scope.enablePan === false ) return; + + handleTouchStartPan( event ); + + state = STATE.TOUCH_PAN; + + break; + + default: + + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + scope.dispatchEvent( startEvent ); + + } + + } + + function onTouchMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + + if ( scope.enableRotate === false ) return; + if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... + + handleTouchMoveRotate( event ); + + break; + + case 2: // two-fingered touch: dolly + + if ( scope.enableZoom === false ) return; + if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... + + handleTouchMoveDolly( event ); + + break; + + case 3: // three-fingered touch: pan + + if ( scope.enablePan === false ) return; + if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... + + handleTouchMovePan( event ); + + break; + + default: + + state = STATE.NONE; + + } + + } + + function onTouchEnd( event ) { + + if ( scope.enabled === false ) return; + + handleTouchEnd( event ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onContextMenu( event ) { + + event.preventDefault(); + + } + + // + + scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); + + scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); + + window.addEventListener( 'keydown', onKeyDown, false ); + + // force an update at start + + this.update(); + +}; + +THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); +THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; + +Object.defineProperties( THREE.OrbitControls.prototype, { + + center: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); + return this.target; + + } + + }, + + // backward compatibility + + noZoom: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); + return ! this.enableZoom; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); + this.enableZoom = ! value; + + } + + }, + + noRotate: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); + return ! this.enableRotate; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); + this.enableRotate = ! value; + + } + + }, + + noPan: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); + return ! this.enablePan; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); + this.enablePan = ! value; + + } + + }, + + noKeys: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); + return ! this.enableKeys; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); + this.enableKeys = ! value; + + } + + }, + + staticMoving: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); + return ! this.enableDamping; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); + this.enableDamping = ! value; + + } + + }, + + dynamicDampingFactor: { + + get: function () { + + console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); + return this.dampingFactor; + + }, + + set: function ( value ) { + + console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); + this.dampingFactor = value; + + } + + } + +} ); diff --git a/client/js/PLYLoader.js b/client/js/PLYLoader.js new file mode 100644 index 0000000..182d71e --- /dev/null +++ b/client/js/PLYLoader.js @@ -0,0 +1,501 @@ +/** + * @author Wei Meng / http://about.me/menway + * + * Description: A THREE loader for PLY ASCII files (known as the Polygon + * File Format or the Stanford Triangle Format). + * + * Limitations: ASCII decoding assumes file is UTF-8. + * + * Usage: + * var loader = new THREE.PLYLoader(); + * loader.load('./models/ply/ascii/dolphins.ply', function (geometry) { + * + * scene.add( new THREE.Mesh( geometry ) ); + * + * } ); + * + * If the PLY file uses non standard property names, they can be mapped while + * loading. For example, the following maps the properties + * “diffuse_(red|green|blue)” in the file to standard color names. + * + * loader.setPropertyNameMapping( { + * diffuse_red: 'red', + * diffuse_green: 'green', + * diffuse_blue: 'blue' + * } ); + * + */ + + +THREE.PLYLoader = function ( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + + this.propertyNameMapping = {}; + +}; + +THREE.PLYLoader.prototype = { + + constructor: THREE.PLYLoader, + + load: function ( url, onLoad, onProgress, onError ) { + + var scope = this; + + var loader = new THREE.FileLoader( this.manager ); + loader.setResponseType( 'arraybuffer' ); + loader.load( url, function ( text ) { + + onLoad( scope.parse( text ) ); + + }, onProgress, onError ); + + }, + + setPropertyNameMapping: function ( mapping ) { + + this.propertyNameMapping = mapping; + + }, + + parse: function ( data ) { + + function isASCII( data ) { + + var header = parseHeader( bin2str( data ) ); + return header.format === 'ascii'; + + } + + function bin2str( buf ) { + + var array_buffer = new Uint8Array( buf ); + var str = ''; + + for ( var i = 0; i < buf.byteLength; i ++ ) { + + str += String.fromCharCode( array_buffer[ i ] ); // implicitly assumes little-endian + + } + + return str; + + } + + function parseHeader( data ) { + + var patternHeader = /ply([\s\S]*)end_header\s/; + var headerText = ''; + var headerLength = 0; + var result = patternHeader.exec( data ); + + if ( result !== null ) { + + headerText = result [ 1 ]; + headerLength = result[ 0 ].length; + + } + + var header = { + comments: [], + elements: [], + headerLength: headerLength + }; + + var lines = headerText.split( '\n' ); + var currentElement; + var lineType, lineValues; + + function make_ply_element_property( propertValues, propertyNameMapping ) { + + var property = { type: propertValues[ 0 ] }; + + if ( property.type === 'list' ) { + + property.name = propertValues[ 3 ]; + property.countType = propertValues[ 1 ]; + property.itemType = propertValues[ 2 ]; + + } else { + + property.name = propertValues[ 1 ]; + + } + + if ( property.name in propertyNameMapping ) { + + property.name = propertyNameMapping[ property.name ]; + + } + + return property; + + } + + for ( var i = 0; i < lines.length; i ++ ) { + + var line = lines[ i ]; + line = line.trim(); + + if ( line === '' ) continue; + + lineValues = line.split( /\s+/ ); + lineType = lineValues.shift(); + line = lineValues.join( ' ' ); + + switch ( lineType ) { + + case 'format': + + header.format = lineValues[ 0 ]; + header.version = lineValues[ 1 ]; + + break; + + case 'comment': + + header.comments.push( line ); + + break; + + case 'element': + + if ( currentElement !== undefined ) { + + header.elements.push( currentElement ); + + } + + currentElement = {}; + currentElement.name = lineValues[ 0 ]; + currentElement.count = parseInt( lineValues[ 1 ] ); + currentElement.properties = []; + + break; + + case 'property': + + currentElement.properties.push( make_ply_element_property( lineValues, scope.propertyNameMapping ) ); + + break; + + + default: + + console.log( 'unhandled', lineType, lineValues ); + + } + + } + + if ( currentElement !== undefined ) { + + header.elements.push( currentElement ); + + } + + return header; + + } + + function parseASCIINumber( n, type ) { + + switch ( type ) { + + case 'char': case 'uchar': case 'short': case 'ushort': case 'int': case 'uint': + case 'int8': case 'uint8': case 'int16': case 'uint16': case 'int32': case 'uint32': + + return parseInt( n ); + + case 'float': case 'double': case 'float32': case 'float64': + + return parseFloat( n ); + + } + + } + + function parseASCIIElement( properties, line ) { + + var values = line.split( /\s+/ ); + + var element = {}; + + for ( var i = 0; i < properties.length; i ++ ) { + + if ( properties[ i ].type === 'list' ) { + + var list = []; + var n = parseASCIINumber( values.shift(), properties[ i ].countType ); + + for ( var j = 0; j < n; j ++ ) { + + list.push( parseASCIINumber( values.shift(), properties[ i ].itemType ) ); + + } + + element[ properties[ i ].name ] = list; + + } else { + + element[ properties[ i ].name ] = parseASCIINumber( values.shift(), properties[ i ].type ); + + } + + } + + return element; + + } + + function parseASCII( data ) { + + // PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format) + + var buffer = { + indices : [], + vertices : [], + normals : [], + uvs : [], + colors : [] + }; + + var result; + + var header = parseHeader( data ); + + var patternBody = /end_header\s([\s\S]*)$/; + var body = ''; + if ( ( result = patternBody.exec( data ) ) !== null ) { + + body = result [ 1 ]; + + } + + var lines = body.split( '\n' ); + var currentElement = 0; + var currentElementCount = 0; + + for ( var i = 0; i < lines.length; i ++ ) { + + var line = lines[ i ]; + line = line.trim(); + if ( line === '' ) { + + continue; + + } + + if ( currentElementCount >= header.elements[ currentElement ].count ) { + + currentElement ++; + currentElementCount = 0; + + } + + var element = parseASCIIElement( header.elements[ currentElement ].properties, line ); + + handleElement( buffer, header.elements[ currentElement ].name, element ); + + currentElementCount ++; + + } + + return postProcess( buffer ); + + } + + function postProcess( buffer ) { + + var geometry = new THREE.BufferGeometry(); + + // mandatory buffer data + + if ( buffer.indices.length > 0 ) { + + geometry.setIndex( buffer.indices ); + + } + + geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( buffer.vertices, 3 ) ); + + // optional buffer data + + if ( buffer.normals.length > 0 ) { + + geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( buffer.normals, 3 ) ); + + } + + if ( buffer.uvs.length > 0 ) { + + geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( buffer.uvs, 2 ) ); + + } + + if ( buffer.colors.length > 0 ) { + + geometry.addAttribute( 'color', new THREE.Float32BufferAttribute( buffer.colors, 3 ) ); + + } + + geometry.computeBoundingSphere(); + + return geometry; + + } + + function handleElement( buffer, elementName, element ) { + + if ( elementName === 'vertex' ) { + + buffer.vertices.push( element.x, element.y, element.z ); + + if ( 'nx' in element && 'ny' in element && 'nz' in element ) { + + buffer.normals.push( element.nx, element.ny, element.nz ); + + } + + if ( 's' in element && 't' in element ) { + + buffer.uvs.push( element.s, element.t ); + + } + + if ( 'red' in element && 'green' in element && 'blue' in element ) { + + buffer.colors.push( element.red / 255.0, element.green / 255.0, element.blue / 255.0 ); + + } + + } else if ( elementName === 'face' ) { + + var vertex_indices = element.vertex_indices || element.vertex_index; // issue #9338 + + if ( vertex_indices.length === 3 ) { + + buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 2 ] ); + + } else if ( vertex_indices.length === 4 ) { + + buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 3 ] ); + buffer.indices.push( vertex_indices[ 1 ], vertex_indices[ 2 ], vertex_indices[ 3 ] ); + + } + + } + + } + + function binaryRead( dataview, at, type, little_endian ) { + + switch ( type ) { + + // corespondences for non-specific length types here match rply: + case 'int8': case 'char': return [ dataview.getInt8( at ), 1 ]; + case 'uint8': case 'uchar': return [ dataview.getUint8( at ), 1 ]; + case 'int16': case 'short': return [ dataview.getInt16( at, little_endian ), 2 ]; + case 'uint16': case 'ushort': return [ dataview.getUint16( at, little_endian ), 2 ]; + case 'int32': case 'int': return [ dataview.getInt32( at, little_endian ), 4 ]; + case 'uint32': case 'uint': return [ dataview.getUint32( at, little_endian ), 4 ]; + case 'float32': case 'float': return [ dataview.getFloat32( at, little_endian ), 4 ]; + case 'float64': case 'double': return [ dataview.getFloat64( at, little_endian ), 8 ]; + + } + + } + + function binaryReadElement( dataview, at, properties, little_endian ) { + + var element = {}; + var result, read = 0; + + for ( var i = 0; i < properties.length; i ++ ) { + + if ( properties[ i ].type === 'list' ) { + + var list = []; + + result = binaryRead( dataview, at + read, properties[ i ].countType, little_endian ); + var n = result[ 0 ]; + read += result[ 1 ]; + + for ( var j = 0; j < n; j ++ ) { + + result = binaryRead( dataview, at + read, properties[ i ].itemType, little_endian ); + list.push( result[ 0 ] ); + read += result[ 1 ]; + + } + + element[ properties[ i ].name ] = list; + + } else { + + result = binaryRead( dataview, at + read, properties[ i ].type, little_endian ); + element[ properties[ i ].name ] = result[ 0 ]; + read += result[ 1 ]; + + } + + } + + return [ element, read ]; + + } + + function parseBinary( data ) { + + var buffer = { + indices : [], + vertices : [], + normals : [], + uvs : [], + colors : [] + }; + + var header = parseHeader( bin2str( data ) ); + var little_endian = ( header.format === 'binary_little_endian' ); + var body = new DataView( data, header.headerLength ); + var result, loc = 0; + + for ( var currentElement = 0; currentElement < header.elements.length; currentElement ++ ) { + + for ( var currentElementCount = 0; currentElementCount < header.elements[ currentElement ].count; currentElementCount ++ ) { + + result = binaryReadElement( body, loc, header.elements[ currentElement ].properties, little_endian ); + loc += result[ 1 ]; + var element = result[ 0 ]; + + handleElement( buffer, header.elements[ currentElement ].name, element ); + + } + + } + + return postProcess( buffer ); + + } + + // + + var geometry; + var scope = this; + + if ( data instanceof ArrayBuffer ) { + + geometry = isASCII( data ) ? parseASCII( bin2str( data ) ) : parseBinary( data ); + + } else { + + geometry = parseASCII( data ); + + } + + return geometry; + + } + +}; diff --git a/client/js/TDSLoader.js b/client/js/TDSLoader.js new file mode 100644 index 0000000..8da8ae2 --- /dev/null +++ b/client/js/TDSLoader.js @@ -0,0 +1,1142 @@ +/* + * Autodesk 3DS threee.js file loader, based on lib3ds. + * + * Loads geometry with uv and materials basic properties with texture support. + * + * @author @tentone + * @author @timknip + * @class TDSLoader + * @constructor + */ + +'use strict'; + +THREE.TDSLoader = function ( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + this.debug = false; + + this.group = null; + this.position = 0; + + this.materials = []; + this.meshes = []; + +}; + +THREE.TDSLoader.prototype = { + + constructor: THREE.TDSLoader, + + /** + * Load 3ds file from url. + * + * @method load + * @param {[type]} url URL for the file. + * @param {Function} onLoad onLoad callback, receives group Object3D as argument. + * @param {Function} onProgress onProgress callback. + * @param {Function} onError onError callback. + */ + load: function ( url, onLoad, onProgress, onError ) { + + var scope = this; + + var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url ); + + var loader = new THREE.FileLoader( this.manager ); + + loader.setResponseType( 'arraybuffer' ); + + loader.load( url, function ( data ) { + + onLoad( scope.parse( data, path ) ); + + }, onProgress, onError ); + + }, + + /** + * Parse arraybuffer data and load 3ds file. + * + * @method parse + * @param {ArrayBuffer} arraybuffer Arraybuffer data to be loaded. + * @param {String} path Path for external resources. + * @return {Object3D} Group loaded from 3ds file. + */ + parse: function ( arraybuffer, path ) { + + this.group = new THREE.Group(); + this.position = 0; + this.materials = []; + this.meshes = []; + + this.readFile( arraybuffer, path ); + + for ( var i = 0; i < this.meshes.length; i ++ ) { + + this.group.add( this.meshes[ i ] ); + + } + + return this.group; + + }, + + /** + * Decode file content to read 3ds data. + * + * @method readFile + * @param {ArrayBuffer} arraybuffer Arraybuffer data to be loaded. + */ + readFile: function ( arraybuffer, path ) { + + var data = new DataView( arraybuffer ); + var chunk = this.readChunk( data ); + + if ( chunk.id === MLIBMAGIC || chunk.id === CMAGIC || chunk.id === M3DMAGIC ) { + + var next = this.nextChunk( data, chunk ); + + while ( next !== 0 ) { + + if ( next === M3D_VERSION ) { + + var version = this.readDWord( data ); + this.debugMessage( '3DS file version: ' + version ); + + } else if ( next === MDATA ) { + + this.resetPosition( data ); + this.readMeshData( data, path ); + + } else { + + this.debugMessage( 'Unknown main chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + } + + this.debugMessage( 'Parsed ' + this.meshes.length + ' meshes' ); + + }, + + /** + * Read mesh data chunk. + * + * @method readMeshData + * @param {Dataview} data Dataview in use. + */ + readMeshData: function ( data, path ) { + + var chunk = this.readChunk( data ); + var next = this.nextChunk( data, chunk ); + + while ( next !== 0 ) { + + if ( next === MESH_VERSION ) { + + var version = + this.readDWord( data ); + this.debugMessage( 'Mesh Version: ' + version ); + + } else if ( next === MASTER_SCALE ) { + + var scale = this.readFloat( data ); + this.debugMessage( 'Master scale: ' + scale ); + this.group.scale.set( scale, scale, scale ); + + } else if ( next === NAMED_OBJECT ) { + + this.debugMessage( 'Named Object' ); + this.resetPosition( data ); + this.readNamedObject( data ); + + } else if ( next === MAT_ENTRY ) { + + this.debugMessage( 'Material' ); + this.resetPosition( data ); + this.readMaterialEntry( data, path ); + + } else { + + this.debugMessage( 'Unknown MDATA chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + }, + + /** + * Read named object chunk. + * + * @method readNamedObject + * @param {Dataview} data Dataview in use. + */ + readNamedObject: function ( data ) { + + var chunk = this.readChunk( data ); + var name = this.readString( data, 64 ); + chunk.cur = this.position; + + var next = this.nextChunk( data, chunk ); + while ( next !== 0 ) { + + if ( next === N_TRI_OBJECT ) { + + this.resetPosition( data ); + var mesh = this.readMesh( data ); + mesh.name = name; + this.meshes.push( mesh ); + + } else { + + this.debugMessage( 'Unknown named object chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + this.endChunk( chunk ); + + }, + + /** + * Read material data chunk and add it to the material list. + * + * @method readMaterialEntry + * @param {Dataview} data Dataview in use. + */ + readMaterialEntry: function ( data, path ) { + + var chunk = this.readChunk( data ); + var next = this.nextChunk( data, chunk ); + var material = new THREE.MeshPhongMaterial(); + + while ( next !== 0 ) { + + if ( next === MAT_NAME ) { + + material.name = this.readString( data, 64 ); + this.debugMessage( ' Name: ' + material.name ); + + } else if ( next === MAT_WIRE ) { + + this.debugMessage( ' Wireframe' ); + material.wireframe = true; + + } else if ( next === MAT_WIRE_SIZE ) { + + var value = this.readByte( data ); + material.wireframeLinewidth = value; + this.debugMessage( ' Wireframe Thickness: ' + value ); + + } else if ( next === MAT_TWO_SIDE ) { + + material.side = THREE.DoubleSide; + this.debugMessage( ' DoubleSided' ); + + } else if ( next === MAT_ADDITIVE ) { + + this.debugMessage( ' Additive Blending' ); + material.blending = THREE.AdditiveBlending; + + } else if ( next === MAT_DIFFUSE ) { + + this.debugMessage( ' Diffuse Color' ); + material.color = this.readColor( data ); + + } else if ( next === MAT_SPECULAR ) { + + this.debugMessage( ' Specular Color' ); + material.specular = this.readColor( data ); + + } else if ( next === MAT_AMBIENT ) { + + this.debugMessage( ' Ambient color' ); + material.color = this.readColor( data ); + + } else if ( next === MAT_SHININESS ) { + + var shininess = this.readWord( data ); + material.shininess = shininess; + this.debugMessage( ' Shininess : ' + shininess ); + + } else if ( next === MAT_TEXMAP ) { + + this.debugMessage( ' ColorMap' ); + this.resetPosition( data ); + material.map = this.readMap( data, path ); + + } else if ( next === MAT_BUMPMAP ) { + + this.debugMessage( ' BumpMap' ); + this.resetPosition( data ); + material.bumpMap = this.readMap( data, path ); + + } else if ( next === MAT_OPACMAP ) { + + this.debugMessage( ' OpacityMap' ); + this.resetPosition( data ); + material.alphaMap = this.readMap( data, path ); + + } else if ( next === MAT_SPECMAP ) { + + this.debugMessage( ' SpecularMap' ); + this.resetPosition( data ); + material.specularMap = this.readMap( data, path ); + + } else { + + this.debugMessage( ' Unknown material chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + this.endChunk( chunk ); + + this.materials[ material.name ] = material; + + }, + + /** + * Read mesh data chunk. + * + * @method readMesh + * @param {Dataview} data Dataview in use. + */ + readMesh: function ( data ) { + + var chunk = this.readChunk( data ); + var next = this.nextChunk( data, chunk ); + + var useBufferGeometry = false; + var geometry = null; + var uvs = []; + + if ( useBufferGeometry ) { + + geometry = new THREE.BufferGeometry(); + + } else { + + geometry = new THREE.Geometry(); + + } + + var material = new THREE.MeshPhongMaterial(); + var mesh = new THREE.Mesh( geometry, material ); + mesh.name = 'mesh'; + + while ( next !== 0 ) { + + if ( next === POINT_ARRAY ) { + + var points = this.readWord( data ); + + this.debugMessage( ' Vertex: ' + points ); + + //BufferGeometry + + if ( useBufferGeometry ) { + + var vertices = []; + for ( var i = 0; i < points; i ++ ) { + + vertices.push( this.readFloat( data ) ); + vertices.push( this.readFloat( data ) ); + vertices.push( this.readFloat( data ) ); + + } + + geometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) ); + + } else { //Geometry + + for ( var i = 0; i < points; i ++ ) { + + geometry.vertices.push( new THREE.Vector3( this.readFloat( data ), this.readFloat( data ), this.readFloat( data ) ) ); + + } + + } + + } else if ( next === FACE_ARRAY ) { + + this.resetPosition( data ); + this.readFaceArray( data, mesh ); + + } else if ( next === TEX_VERTS ) { + + var texels = this.readWord( data ); + + this.debugMessage( ' UV: ' + texels ); + + //BufferGeometry + + if ( useBufferGeometry ) { + + var uvs = []; + for ( var i = 0; i < texels; i ++ ) { + + uvs.push( this.readFloat( data ) ); + uvs.push( this.readFloat( data ) ); + + } + geometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( uvs ), 2 ) ); + + } else { //Geometry + + uvs = []; + for ( var i = 0; i < texels; i ++ ) { + + uvs.push( new THREE.Vector2( this.readFloat( data ), this.readFloat( data ) ) ); + + } + + } + + } else if ( next === MESH_MATRIX ) { + + this.debugMessage( ' Tranformation Matrix (TODO)' ); + + var values = []; + for ( var i = 0; i < 12; i ++ ) { + + values[ i ] = this.readFloat( data ); + + } + + var matrix = new THREE.Matrix4(); + + //X Line + matrix.elements[ 0 ] = values[ 0 ]; + matrix.elements[ 1 ] = values[ 6 ]; + matrix.elements[ 2 ] = values[ 3 ]; + matrix.elements[ 3 ] = values[ 9 ]; + + //Y Line + matrix.elements[ 4 ] = values[ 2 ]; + matrix.elements[ 5 ] = values[ 8 ]; + matrix.elements[ 6 ] = values[ 5 ]; + matrix.elements[ 7 ] = values[ 11 ]; + + //Z Line + matrix.elements[ 8 ] = values[ 1 ]; + matrix.elements[ 9 ] = values[ 7 ]; + matrix.elements[ 10 ] = values[ 4 ]; + matrix.elements[ 11 ] = values[ 10 ]; + + //W Line + matrix.elements[ 12 ] = 0; + matrix.elements[ 13 ] = 0; + matrix.elements[ 14 ] = 0; + matrix.elements[ 15 ] = 1; + + matrix.transpose(); + + var inverse = new THREE.Matrix4(); + inverse.getInverse( matrix, true ); + geometry.applyMatrix( inverse ); + + matrix.decompose( mesh.position, mesh.quaternion, mesh.scale ); + + } else { + + this.debugMessage( ' Unknown mesh chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + this.endChunk( chunk ); + + if ( ! useBufferGeometry ) { + + //geometry.faceVertexUvs[0][faceIndex][vertexIndex] + + if ( uvs.length > 0 ) { + + var faceUV = []; + + for ( var i = 0; i < geometry.faces.length; i ++ ) { + + faceUV.push( [ uvs[ geometry.faces[ i ].a ], uvs[ geometry.faces[ i ].b ], uvs[ geometry.faces[ i ].c ] ] ); + + } + + geometry.faceVertexUvs[ 0 ] = faceUV; + + } + + geometry.computeVertexNormals(); + + } + + return mesh; + + }, + + /** + * Read face array data chunk. + * + * @method readFaceArray + * @param {Dataview} data Dataview in use. + * @param {Mesh} mesh Mesh to be filled with the data read. + */ + readFaceArray: function ( data, mesh ) { + + var chunk = this.readChunk( data ); + var faces = this.readWord( data ); + + this.debugMessage( ' Faces: ' + faces ); + + for ( var i = 0; i < faces; ++ i ) { + + mesh.geometry.faces.push( new THREE.Face3( this.readWord( data ), this.readWord( data ), this.readWord( data ) ) ); + + var visibility = this.readWord( data ); + + } + + //The rest of the FACE_ARRAY chunk is subchunks + + while ( this.position < chunk.end ) { + + var chunk = this.readChunk( data ); + + if ( chunk.id === MSH_MAT_GROUP ) { + + this.debugMessage( ' Material Group' ); + + this.resetPosition( data ); + + var group = this.readMaterialGroup( data ); + + var material = this.materials[ group.name ]; + + if ( material !== undefined ) { + + mesh.material = material; + + if ( material.name === '' ) { + + material.name = mesh.name; + + } + + } + + } else { + + this.debugMessage( ' Unknown face array chunk: ' + chunk.toString( 16 ) ); + + } + + this.endChunk( chunk ); + + } + + this.endChunk( chunk ); + + }, + + /** + * Read texture map data chunk. + * + * @method readMap + * @param {Dataview} data Dataview in use. + * @return {Texture} Texture read from this data chunk. + */ + readMap: function ( data, path ) { + + var chunk = this.readChunk( data ); + var next = this.nextChunk( data, chunk ); + var texture = {}; + + var loader = new THREE.TextureLoader( this.manager ); + loader.setPath( path ); + + while ( next !== 0 ) { + + if ( next === MAT_MAPNAME ) { + + var name = this.readString( data, 128 ); + texture = loader.load( name ); + + this.debugMessage( ' File: ' + path + name ); + + } else if ( next === MAT_MAP_UOFFSET ) { + + texture.offset.x = this.readFloat( data ); + this.debugMessage( ' OffsetX: ' + texture.offset.x ); + + } else if ( next === MAT_MAP_VOFFSET ) { + + texture.offset.y = this.readFloat( data ); + this.debugMessage( ' OffsetY: ' + texture.offset.y ); + + } else if ( next === MAT_MAP_USCALE ) { + + texture.repeat.x = this.readFloat( data ); + this.debugMessage( ' RepeatX: ' + texture.repeat.x ); + + } else if ( next === MAT_MAP_VSCALE ) { + + texture.repeat.y = this.readFloat( data ); + this.debugMessage( ' RepeatY: ' + texture.repeat.y ); + + } else { + + this.debugMessage( ' Unknown map chunk: ' + next.toString( 16 ) ); + + } + + next = this.nextChunk( data, chunk ); + + } + + this.endChunk( chunk ); + + return texture; + + }, + + /** + * Read material group data chunk. + * + * @method readMaterialGroup + * @param {Dataview} data Dataview in use. + * @return {Object} Object with name and index of the object. + */ + readMaterialGroup: function ( data ) { + + var chunk = this.readChunk( data ); + var name = this.readString( data, 64 ); + var numFaces = this.readWord( data ); + + this.debugMessage( ' Name: ' + name ); + this.debugMessage( ' Faces: ' + numFaces ); + + var index = []; + for ( var i = 0; i < numFaces; ++ i ) { + + index.push( this.readWord( data ) ); + + } + + return { name: name, index: index }; + + }, + + /** + * Read a color value. + * + * @method readColor + * @param {DataView} data Dataview. + * @return {Color} Color value read.. + */ + readColor: function ( data ) { + + var chunk = this.readChunk( data ); + var color = new THREE.Color(); + + if ( chunk.id === COLOR_24 || chunk.id === LIN_COLOR_24 ) { + + var r = this.readByte( data ); + var g = this.readByte( data ); + var b = this.readByte( data ); + + color.setRGB( r / 255, g / 255, b / 255 ); + + this.debugMessage( ' Color: ' + color.r + ', ' + color.g + ', ' + color.b ); + + } else if ( chunk.id === COLOR_F || chunk.id === LIN_COLOR_F ) { + + var r = this.readFloat( data ); + var g = this.readFloat( data ); + var b = this.readFloat( data ); + + color.setRGB( r, g, b ); + + this.debugMessage( ' Color: ' + color.r + ', ' + color.g + ', ' + color.b ); + + } else { + + this.debugMessage( ' Unknown color chunk: ' + chunk.toString( 16 ) ); + + } + + this.endChunk( chunk ); + return color; + + }, + + /** + * Read next chunk of data. + * + * @method readChunk + * @param {DataView} data Dataview. + * @return {Object} Chunk of data read. + */ + readChunk: function ( data ) { + + var chunk = {}; + + chunk.cur = this.position; + chunk.id = this.readWord( data ); + chunk.size = this.readDWord( data ); + chunk.end = chunk.cur + chunk.size; + chunk.cur += 6; + + return chunk; + + }, + + /** + * Set position to the end of the current chunk of data. + * + * @method endChunk + * @param {Object} chunk Data chunk. + */ + endChunk: function ( chunk ) { + + this.position = chunk.end; + + }, + + /** + * Move to the next data chunk. + * + * @method nextChunk + * @param {DataView} data Dataview. + * @param {Object} chunk Data chunk. + */ + nextChunk: function ( data, chunk ) { + + if ( chunk.cur >= chunk.end ) { + + return 0; + + } + + this.position = chunk.cur; + + try { + + var next = this.readChunk( data ); + chunk.cur += next.size; + return next.id; + + } catch ( e ) { + + this.debugMessage( 'Unable to read chunk at ' + this.position ); + return 0; + + } + + }, + + /** + * Reset dataview position. + * + * @method resetPosition + * @param {DataView} data Dataview. + */ + resetPosition: function () { + + this.position -= 6; + + }, + + /** + * Read byte value. + * + * @method readByte + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readByte: function ( data ) { + + var v = data.getUint8( this.position, true ); + this.position += 1; + return v; + + }, + + /** + * Read 32 bit float value. + * + * @method readFloat + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readFloat: function ( data ) { + + try { + + var v = data.getFloat32( this.position, true ); + this.position += 4; + return v; + + } catch ( e ) { + + this.debugMessage( e + ' ' + this.position + ' ' + data.byteLength ); + + } + + }, + + /** + * Read 32 bit signed integer value. + * + * @method readInt + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readInt: function ( data ) { + + var v = data.getInt32( this.position, true ); + this.position += 4; + return v; + + }, + + /** + * Read 16 bit signed integer value. + * + * @method readShort + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readShort: function ( data ) { + + var v = data.getInt16( this.position, true ); + this.position += 2; + return v; + + }, + + /** + * Read 64 bit unsigned integer value. + * + * @method readDWord + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readDWord: function ( data ) { + + var v = data.getUint32( this.position, true ); + this.position += 4; + return v; + + }, + + /** + * Read 32 bit unsigned integer value. + * + * @method readWord + * @param {DataView} data Dataview to read data from. + * @return {Number} Data read from the dataview. + */ + readWord: function ( data ) { + + var v = data.getUint16( this.position, true ); + this.position += 2; + return v; + + }, + + /** + * Read string value. + * + * @method readString + * @param {DataView} data Dataview to read data from. + * @param {Number} maxLength Max size of the string to be read. + * @return {String} Data read from the dataview. + */ + readString: function ( data, maxLength ) { + + var s = ''; + + for ( var i = 0; i < maxLength; i ++ ) { + + var c = this.readByte( data ); + if ( ! c ) { + + break; + + } + + s += String.fromCharCode( c ); + + } + + return s; + + }, + + /** + * Set resource path used to determine the file path to attached resources. + * + * @method setPath + * @param {String} path Path to resources. + * @return Self for chaining. + */ + setPath: function ( path ) { + + this.path = path; + + return this; + + }, + + /** + * Print debug message to the console. + * + * Is controlled by a flag to show or hide debug messages. + * + * @method debugMessage + * @param {Object} message Debug message to print to the console. + */ + debugMessage: function ( message ) { + + if ( this.debug ) { + + console.log( message ); + + } + + } +}; + +var NULL_CHUNK = 0x0000; +var M3DMAGIC = 0x4D4D; +var SMAGIC = 0x2D2D; +var LMAGIC = 0x2D3D; +var MLIBMAGIC = 0x3DAA; +var MATMAGIC = 0x3DFF; +var CMAGIC = 0xC23D; +var M3D_VERSION = 0x0002; +var M3D_KFVERSION = 0x0005; +var COLOR_F = 0x0010; +var COLOR_24 = 0x0011; +var LIN_COLOR_24 = 0x0012; +var LIN_COLOR_F = 0x0013; +var INT_PERCENTAGE = 0x0030; +var FLOAT_PERCENTAGE = 0x0031; +var MDATA = 0x3D3D; +var MESH_VERSION = 0x3D3E; +var MASTER_SCALE = 0x0100; +var LO_SHADOW_BIAS = 0x1400; +var HI_SHADOW_BIAS = 0x1410; +var SHADOW_MAP_SIZE = 0x1420; +var SHADOW_SAMPLES = 0x1430; +var SHADOW_RANGE = 0x1440; +var SHADOW_FILTER = 0x1450; +var RAY_BIAS = 0x1460; +var O_CONSTS = 0x1500; +var AMBIENT_LIGHT = 0x2100; +var BIT_MAP = 0x1100; +var SOLID_BGND = 0x1200; +var V_GRADIENT = 0x1300; +var USE_BIT_MAP = 0x1101; +var USE_SOLID_BGND = 0x1201; +var USE_V_GRADIENT = 0x1301; +var FOG = 0x2200; +var FOG_BGND = 0x2210; +var LAYER_FOG = 0x2302; +var DISTANCE_CUE = 0x2300; +var DCUE_BGND = 0x2310; +var USE_FOG = 0x2201; +var USE_LAYER_FOG = 0x2303; +var USE_DISTANCE_CUE = 0x2301; +var MAT_ENTRY = 0xAFFF; +var MAT_NAME = 0xA000; +var MAT_AMBIENT = 0xA010; +var MAT_DIFFUSE = 0xA020; +var MAT_SPECULAR = 0xA030; +var MAT_SHININESS = 0xA040; +var MAT_SHIN2PCT = 0xA041; +var MAT_TRANSPARENCY = 0xA050; +var MAT_XPFALL = 0xA052; +var MAT_USE_XPFALL = 0xA240; +var MAT_REFBLUR = 0xA053; +var MAT_SHADING = 0xA100; +var MAT_USE_REFBLUR = 0xA250; +var MAT_SELF_ILLUM = 0xA084; +var MAT_TWO_SIDE = 0xA081; +var MAT_DECAL = 0xA082; +var MAT_ADDITIVE = 0xA083; +var MAT_WIRE = 0xA085; +var MAT_FACEMAP = 0xA088; +var MAT_TRANSFALLOFF_IN = 0xA08A; +var MAT_PHONGSOFT = 0xA08C; +var MAT_WIREABS = 0xA08E; +var MAT_WIRE_SIZE = 0xA087; +var MAT_TEXMAP = 0xA200; +var MAT_SXP_TEXT_DATA = 0xA320; +var MAT_TEXMASK = 0xA33E; +var MAT_SXP_TEXTMASK_DATA = 0xA32A; +var MAT_TEX2MAP = 0xA33A; +var MAT_SXP_TEXT2_DATA = 0xA321; +var MAT_TEX2MASK = 0xA340; +var MAT_SXP_TEXT2MASK_DATA = 0xA32C; +var MAT_OPACMAP = 0xA210; +var MAT_SXP_OPAC_DATA = 0xA322; +var MAT_OPACMASK = 0xA342; +var MAT_SXP_OPACMASK_DATA = 0xA32E; +var MAT_BUMPMAP = 0xA230; +var MAT_SXP_BUMP_DATA = 0xA324; +var MAT_BUMPMASK = 0xA344; +var MAT_SXP_BUMPMASK_DATA = 0xA330; +var MAT_SPECMAP = 0xA204; +var MAT_SXP_SPEC_DATA = 0xA325; +var MAT_SPECMASK = 0xA348; +var MAT_SXP_SPECMASK_DATA = 0xA332; +var MAT_SHINMAP = 0xA33C; +var MAT_SXP_SHIN_DATA = 0xA326; +var MAT_SHINMASK = 0xA346; +var MAT_SXP_SHINMASK_DATA = 0xA334; +var MAT_SELFIMAP = 0xA33D; +var MAT_SXP_SELFI_DATA = 0xA328; +var MAT_SELFIMASK = 0xA34A; +var MAT_SXP_SELFIMASK_DATA = 0xA336; +var MAT_REFLMAP = 0xA220; +var MAT_REFLMASK = 0xA34C; +var MAT_SXP_REFLMASK_DATA = 0xA338; +var MAT_ACUBIC = 0xA310; +var MAT_MAPNAME = 0xA300; +var MAT_MAP_TILING = 0xA351; +var MAT_MAP_TEXBLUR = 0xA353; +var MAT_MAP_USCALE = 0xA354; +var MAT_MAP_VSCALE = 0xA356; +var MAT_MAP_UOFFSET = 0xA358; +var MAT_MAP_VOFFSET = 0xA35A; +var MAT_MAP_ANG = 0xA35C; +var MAT_MAP_COL1 = 0xA360; +var MAT_MAP_COL2 = 0xA362; +var MAT_MAP_RCOL = 0xA364; +var MAT_MAP_GCOL = 0xA366; +var MAT_MAP_BCOL = 0xA368; +var NAMED_OBJECT = 0x4000; +var N_DIRECT_LIGHT = 0x4600; +var DL_OFF = 0x4620; +var DL_OUTER_RANGE = 0x465A; +var DL_INNER_RANGE = 0x4659; +var DL_MULTIPLIER = 0x465B; +var DL_EXCLUDE = 0x4654; +var DL_ATTENUATE = 0x4625; +var DL_SPOTLIGHT = 0x4610; +var DL_SPOT_ROLL = 0x4656; +var DL_SHADOWED = 0x4630; +var DL_LOCAL_SHADOW2 = 0x4641; +var DL_SEE_CONE = 0x4650; +var DL_SPOT_RECTANGULAR = 0x4651; +var DL_SPOT_ASPECT = 0x4657; +var DL_SPOT_PROJECTOR = 0x4653; +var DL_SPOT_OVERSHOOT = 0x4652; +var DL_RAY_BIAS = 0x4658; +var DL_RAYSHAD = 0x4627; +var N_CAMERA = 0x4700; +var CAM_SEE_CONE = 0x4710; +var CAM_RANGES = 0x4720; +var OBJ_HIDDEN = 0x4010; +var OBJ_VIS_LOFTER = 0x4011; +var OBJ_DOESNT_CAST = 0x4012; +var OBJ_DONT_RECVSHADOW = 0x4017; +var OBJ_MATTE = 0x4013; +var OBJ_FAST = 0x4014; +var OBJ_PROCEDURAL = 0x4015; +var OBJ_FROZEN = 0x4016; +var N_TRI_OBJECT = 0x4100; +var POINT_ARRAY = 0x4110; +var POINT_FLAG_ARRAY = 0x4111; +var FACE_ARRAY = 0x4120; +var MSH_MAT_GROUP = 0x4130; +var SMOOTH_GROUP = 0x4150; +var MSH_BOXMAP = 0x4190; +var TEX_VERTS = 0x4140; +var MESH_MATRIX = 0x4160; +var MESH_COLOR = 0x4165; +var MESH_TEXTURE_INFO = 0x4170; +var KFDATA = 0xB000; +var KFHDR = 0xB00A; +var KFSEG = 0xB008; +var KFCURTIME = 0xB009; +var AMBIENT_NODE_TAG = 0xB001; +var OBJECT_NODE_TAG = 0xB002; +var CAMERA_NODE_TAG = 0xB003; +var TARGET_NODE_TAG = 0xB004; +var LIGHT_NODE_TAG = 0xB005; +var L_TARGET_NODE_TAG = 0xB006; +var SPOTLIGHT_NODE_TAG = 0xB007; +var NODE_ID = 0xB030; +var NODE_HDR = 0xB010; +var PIVOT = 0xB013; +var INSTANCE_NAME = 0xB011; +var MORPH_SMOOTH = 0xB015; +var BOUNDBOX = 0xB014; +var POS_TRACK_TAG = 0xB020; +var COL_TRACK_TAG = 0xB025; +var ROT_TRACK_TAG = 0xB021; +var SCL_TRACK_TAG = 0xB022; +var MORPH_TRACK_TAG = 0xB026; +var FOV_TRACK_TAG = 0xB023; +var ROLL_TRACK_TAG = 0xB024; +var HOT_TRACK_TAG = 0xB027; +var FALL_TRACK_TAG = 0xB028; +var HIDE_TRACK_TAG = 0xB029; +var POLY_2D = 0x5000; +var SHAPE_OK = 0x5010; +var SHAPE_NOT_OK = 0x5011; +var SHAPE_HOOK = 0x5020; +var PATH_3D = 0x6000; +var PATH_MATRIX = 0x6005; +var SHAPE_2D = 0x6010; +var M_SCALE = 0x6020; +var M_TWIST = 0x6030; +var M_TEETER = 0x6040; +var M_FIT = 0x6050; +var M_BEVEL = 0x6060; +var XZ_CURVE = 0x6070; +var YZ_CURVE = 0x6080; +var INTERPCT = 0x6090; +var DEFORM_LIMIT = 0x60A0; +var USE_CONTOUR = 0x6100; +var USE_TWEEN = 0x6110; +var USE_SCALE = 0x6120; +var USE_TWIST = 0x6130; +var USE_TEETER = 0x6140; +var USE_FIT = 0x6150; +var USE_BEVEL = 0x6160; +var DEFAULT_VIEW = 0x3000; +var VIEW_TOP = 0x3010; +var VIEW_BOTTOM = 0x3020; +var VIEW_LEFT = 0x3030; +var VIEW_RIGHT = 0x3040; +var VIEW_FRONT = 0x3050; +var VIEW_BACK = 0x3060; +var VIEW_USER = 0x3070; +var VIEW_CAMERA = 0x3080; +var VIEW_WINDOW = 0x3090; +var VIEWPORT_LAYOUT_OLD = 0x7000; +var VIEWPORT_DATA_OLD = 0x7010; +var VIEWPORT_LAYOUT = 0x7001; +var VIEWPORT_DATA = 0x7011; +var VIEWPORT_DATA_3 = 0x7012; +var VIEWPORT_SIZE = 0x7020; +var NETWORK_VIEW = 0x7030; diff --git a/client/js/Treant.js b/client/js/Treant.js new file mode 100644 index 0000000..beb061c --- /dev/null +++ b/client/js/Treant.js @@ -0,0 +1,2171 @@ +/* + * Treant-js + * + * (c) 2013 Fran Peručić + * Treant-js may be freely distributed under the MIT license. + * For all details and documentation: + * http://fperucic.github.io/treant-js + * + * Treant is an open-source JavaScipt library for visualization of tree diagrams. + * It implements the node positioning algorithm of John Q. Walker II "Positioning nodes for General Trees". + * + * References: + * Emilio Cortegoso Lobato: ECOTree.js v1.0 (October 26th, 2006) + * + * Contributors: + * Fran Peručić, https://github.com/fperucic + * Dave Goodchild, https://github.com/dlgoodchild + */ + +;( function() { + // Polyfill for IE to use startsWith + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(searchString, position){ + return this.substr(position || 0, searchString.length) === searchString; + }; + } + + var $ = null; + + var UTIL = { + + /** + * Directly updates, recursively/deeply, the first object with all properties in the second object + * @param {object} applyTo + * @param {object} applyFrom + * @return {object} + */ + inheritAttrs: function( applyTo, applyFrom ) { + for ( var attr in applyFrom ) { + if ( applyFrom.hasOwnProperty( attr ) ) { + if ( ( applyTo[attr] instanceof Object && applyFrom[attr] instanceof Object ) && ( typeof applyFrom[attr] !== 'function' ) ) { + this.inheritAttrs( applyTo[attr], applyFrom[attr] ); + } + else { + applyTo[attr] = applyFrom[attr]; + } + } + } + return applyTo; + }, + + /** + * Returns a new object by merging the two supplied objects + * @param {object} obj1 + * @param {object} obj2 + * @returns {object} + */ + createMerge: function( obj1, obj2 ) { + var newObj = {}; + if ( obj1 ) { + this.inheritAttrs( newObj, this.cloneObj( obj1 ) ); + } + if ( obj2 ) { + this.inheritAttrs( newObj, obj2 ); + } + return newObj; + }, + + /** + * Takes any number of arguments + * @returns {*} + */ + extend: function() { + if ( $ ) { + Array.prototype.unshift.apply( arguments, [true, {}] ); + return $.extend.apply( $, arguments ); + } + else { + return UTIL.createMerge.apply( this, arguments ); + } + }, + + /** + * @param {object} obj + * @returns {*} + */ + cloneObj: function ( obj ) { + if ( Object( obj ) !== obj ) { + return obj; + } + var res = new obj.constructor(); + for ( var key in obj ) { + if ( obj.hasOwnProperty(key) ) { + res[key] = this.cloneObj(obj[key]); + } + } + return res; + }, + + /** + * @param {Element} el + * @param {string} eventType + * @param {function} handler + */ + addEvent: function( el, eventType, handler ) { + if ( $ ) { + $( el ).on( eventType+'.treant', handler ); + } + else if ( el.addEventListener ) { // DOM Level 2 browsers + el.addEventListener( eventType, handler, false ); + } + else if ( el.attachEvent ) { // IE <= 8 + el.attachEvent( 'on' + eventType, handler ); + } + else { // ancient browsers + el['on' + eventType] = handler; + } + }, + + /** + * @param {string} selector + * @param {boolean} raw + * @param {Element} parentEl + * @returns {Element|jQuery} + */ + findEl: function( selector, raw, parentEl ) { + parentEl = parentEl || document; + + if ( $ ) { + var $element = $( selector, parentEl ); + return ( raw? $element.get( 0 ): $element ); + } + else { + // todo: getElementsByName() + // todo: getElementsByTagName() + // todo: getElementsByTagNameNS() + if ( selector.charAt( 0 ) === '#' ) { + return parentEl.getElementById( selector.substring( 1 ) ); + } + else if ( selector.charAt( 0 ) === '.' ) { + var oElements = parentEl.getElementsByClassName( selector.substring( 1 ) ); + return ( oElements.length? oElements[0]: null ); + } + + throw new Error( 'Unknown container element' ); + } + }, + + getOuterHeight: function( element ) { + var nRoundingCompensation = 1; + if ( typeof element.getBoundingClientRect === 'function' ) { + return element.getBoundingClientRect().height; + } + else if ( $ ) { + return Math.ceil( $( element ).outerHeight() ) + nRoundingCompensation; + } + else { + return Math.ceil( + element.clientHeight + + UTIL.getStyle( element, 'border-top-width', true ) + + UTIL.getStyle( element, 'border-bottom-width', true ) + + UTIL.getStyle( element, 'padding-top', true ) + + UTIL.getStyle( element, 'padding-bottom', true ) + + nRoundingCompensation + ); + } + }, + + getOuterWidth: function( element ) { + var nRoundingCompensation = 1; + if ( typeof element.getBoundingClientRect === 'function' ) { + return element.getBoundingClientRect().width; + } + else if ( $ ) { + return Math.ceil( $( element ).outerWidth() ) + nRoundingCompensation; + } + else { + return Math.ceil( + element.clientWidth + + UTIL.getStyle( element, 'border-left-width', true ) + + UTIL.getStyle( element, 'border-right-width', true ) + + UTIL.getStyle( element, 'padding-left', true ) + + UTIL.getStyle( element, 'padding-right', true ) + + nRoundingCompensation + ); + } + }, + + getStyle: function( element, strCssRule, asInt ) { + var strValue = ""; + if ( document.defaultView && document.defaultView.getComputedStyle ) { + strValue = document.defaultView.getComputedStyle( element, '' ).getPropertyValue( strCssRule ); + } + else if( element.currentStyle ) { + strCssRule = strCssRule.replace(/\-(\w)/g, + function (strMatch, p1){ + return p1.toUpperCase(); + } + ); + strValue = element.currentStyle[strCssRule]; + } + //Number(elem.style.width.replace(/[^\d\.\-]/g, '')); + return ( asInt? parseFloat( strValue ): strValue ); + }, + + addClass: function( element, cssClass ) { + if ( $ ) { + $( element ).addClass( cssClass ); + } + else { + if ( !UTIL.hasClass( element, cssClass ) ) { + if ( element.classList ) { + element.classList.add( cssClass ); + } + else { + element.className += " "+cssClass; + } + } + } + }, + + hasClass: function(element, my_class) { + return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(" "+my_class+" ") > -1; + }, + + toggleClass: function ( element, cls, apply ) { + if ( $ ) { + $( element ).toggleClass( cls, apply ); + } + else { + if ( apply ) { + //element.className += " "+cls; + element.classList.add( cls ); + } + else { + element.classList.remove( cls ); + } + } + }, + + setDimensions: function( element, width, height ) { + if ( $ ) { + $( element ).width( width ).height( height ); + } + else { + element.style.width = width+'px'; + element.style.height = height+'px'; + } + }, + isjQueryAvailable: function() {return(typeof ($) !== 'undefined' && $);}, + }; + + /** + * ImageLoader is used for determining if all the images from the Tree are loaded. + * Node size (width, height) can be correctly determined only when all inner images are loaded + */ + var ImageLoader = function() { + this.reset(); + }; + + ImageLoader.prototype = { + + /** + * @returns {ImageLoader} + */ + reset: function() { + this.loading = []; + return this; + }, + + /** + * @param {TreeNode} node + * @returns {ImageLoader} + */ + processNode: function( node ) { + var aImages = node.nodeDOM.getElementsByTagName( 'img' ); + + var i = aImages.length; + while ( i-- ) { + this.create( node, aImages[i] ); + } + return this; + }, + + /** + * @returns {ImageLoader} + */ + removeAll: function( img_src ) { + var i = this.loading.length; + while ( i-- ) { + if ( this.loading[i] === img_src ) { + this.loading.splice( i, 1 ); + } + } + return this; + }, + + /** + * @param {TreeNode} node + * @param {Element} image + * @returns {*} + */ + create: function ( node, image ) { + var self = this, source = image.src; + + function imgTrigger() { + self.removeAll( source ); + node.width = node.nodeDOM.offsetWidth; + node.height = node.nodeDOM.offsetHeight; + } + + if ( image.src.indexOf( 'data:' ) !== 0 ) { + this.loading.push( source ); + + if ( image.complete ) { + return imgTrigger(); + } + + UTIL.addEvent( image, 'load', imgTrigger ); + UTIL.addEvent( image, 'error', imgTrigger ); // handle broken url-s + + // load event is not fired for cached images, force the load event + image.src += ( ( image.src.indexOf( '?' ) > 0)? '&': '?' ) + new Date().getTime(); + } + else { + imgTrigger(); + } + }, + + /** + * @returns {boolean} + */ + isNotLoading: function() { + return ( this.loading.length === 0 ); + } + }; + + /** + * Class: TreeStore + * TreeStore is used for holding initialized Tree objects + * Its purpose is to avoid global variables and enable multiple Trees on the page. + */ + var TreeStore = { + + store: [], + + /** + * @param {object} jsonConfig + * @returns {Tree} + */ + createTree: function( jsonConfig ) { + var nNewTreeId = this.store.length; + this.store.push( new Tree( jsonConfig, nNewTreeId ) ); + return this.get( nNewTreeId ); + }, + + /** + * @param {number} treeId + * @returns {Tree} + */ + get: function ( treeId ) { + return this.store[treeId]; + }, + + /** + * @param {number} treeId + * @returns {TreeStore} + */ + destroy: function( treeId ) { + var tree = this.get( treeId ); + if ( tree ) { + tree._R.remove(); + var draw_area = tree.drawArea; + + while ( draw_area.firstChild ) { + draw_area.removeChild( draw_area.firstChild ); + } + + var classes = draw_area.className.split(' '), + classes_to_stay = []; + + for ( var i = 0; i < classes.length; i++ ) { + var cls = classes[i]; + if ( cls !== 'Treant' && cls !== 'Treant-loaded' ) { + classes_to_stay.push(cls); + } + } + draw_area.style.overflowY = ''; + draw_area.style.overflowX = ''; + draw_area.className = classes_to_stay.join(' '); + + this.store[treeId] = null; + } + return this; + } + }; + + /** + * Tree constructor. + * @param {object} jsonConfig + * @param {number} treeId + * @constructor + */ + var Tree = function (jsonConfig, treeId ) { + + /** + * @param {object} jsonConfig + * @param {number} treeId + * @returns {Tree} + */ + this.reset = function( jsonConfig, treeId ) { + this.initJsonConfig = jsonConfig; + this.initTreeId = treeId; + + this.id = treeId; + + this.CONFIG = UTIL.extend( Tree.CONFIG, jsonConfig.chart ); + this.drawArea = UTIL.findEl( this.CONFIG.container, true ); + if ( !this.drawArea ) { + throw new Error( 'Failed to find element by selector "'+this.CONFIG.container+'"' ); + } + + UTIL.addClass( this.drawArea, 'Treant' ); + + // kill of any child elements that may be there + this.drawArea.innerHTML = ''; + + this.imageLoader = new ImageLoader(); + + this.nodeDB = new NodeDB( jsonConfig.nodeStructure, this ); + + // key store for storing reference to node connectors, + // key = nodeId where the connector ends + this.connectionStore = {}; + + this.loaded = false; + + this._R = new Raphael( this.drawArea, 100, 100 ); + + return this; + }; + + /** + * @returns {Tree} + */ + this.reload = function() { + this.reset( this.initJsonConfig, this.initTreeId ).redraw(); + return this; + }; + + this.reset( jsonConfig, treeId ); + }; + + Tree.prototype = { + + /** + * @returns {NodeDB} + */ + getNodeDb: function() { + return this.nodeDB; + }, + + /** + * @param {TreeNode} parentTreeNode + * @param {object} nodeDefinition + * @returns {TreeNode} + */ + addNode: function( parentTreeNode, nodeDefinition ) { + var dbEntry = this.nodeDB.get( parentTreeNode.id ); + + this.CONFIG.callback.onBeforeAddNode.apply( this, [parentTreeNode, nodeDefinition] ); + + var oNewNode = this.nodeDB.createNode( nodeDefinition, parentTreeNode.id, this ); + oNewNode.createGeometry( this ); + + oNewNode.parent().createSwitchGeometry( this ); + + this.positionTree(); + + this.CONFIG.callback.onAfterAddNode.apply( this, [oNewNode, parentTreeNode, nodeDefinition] ); + + return oNewNode; + }, + + /** + * @returns {Tree} + */ + redraw: function() { + this.positionTree(); + return this; + }, + + /** + * @param {function} callback + * @returns {Tree} + */ + positionTree: function( callback ) { + var self = this; + + if ( this.imageLoader.isNotLoading() ) { + var root = this.root(), + orient = this.CONFIG.rootOrientation; + + this.resetLevelData(); + + this.firstWalk( root, 0 ); + this.secondWalk( root, 0, 0, 0 ); + + this.positionNodes(); + + if ( this.CONFIG.animateOnInit ) { + setTimeout( + function() { + root.toggleCollapse(); + }, + this.CONFIG.animateOnInitDelay + ); + } + + if ( !this.loaded ) { + UTIL.addClass( this.drawArea, 'Treant-loaded' ); // nodes are hidden until .loaded class is added + if ( Object.prototype.toString.call( callback ) === "[object Function]" ) { + callback( self ); + } + self.CONFIG.callback.onTreeLoaded.apply( self, [root] ); + this.loaded = true; + } + + } + else { + setTimeout( + function() { + self.positionTree( callback ); + }, 10 + ); + } + return this; + }, + + /** + * In a first post-order walk, every node of the tree is assigned a preliminary + * x-coordinate (held in field node->prelim). + * In addition, internal nodes are given modifiers, which will be used to move their + * children to the right (held in field node->modifier). + * @param {TreeNode} node + * @param {number} level + * @returns {Tree} + */ + firstWalk: function( node, level ) { + node.prelim = null; + node.modifier = null; + + this.setNeighbors( node, level ); + this.calcLevelDim( node, level ); + + var leftSibling = node.leftSibling(); + + if ( node.childrenCount() === 0 || level == this.CONFIG.maxDepth ) { + // set preliminary x-coordinate + if ( leftSibling ) { + node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; + } + else { + node.prelim = 0; + } + } + else { + //node is not a leaf, firstWalk for each child + for ( var i = 0, n = node.childrenCount(); i < n; i++ ) { + this.firstWalk(node.childAt(i), level + 1); + } + + var midPoint = node.childrenCenter() - node.size() / 2; + + if ( leftSibling ) { + node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; + node.modifier = node.prelim - midPoint; + this.apportion( node, level ); + } + else { + node.prelim = midPoint; + } + + // handle stacked children positioning + if ( node.stackParent ) { // handle the parent of stacked children + node.modifier += this.nodeDB.get( node.stackChildren[0] ).size()/2 + node.connStyle.stackIndent; + } + else if ( node.stackParentId ) { // handle stacked children + node.prelim = 0; + } + } + return this; + }, + + /* + * Clean up the positioning of small sibling subtrees. + * Subtrees of a node are formed independently and + * placed as close together as possible. By requiring + * that the subtrees be rigid at the time they are put + * together, we avoid the undesirable effects that can + * accrue from positioning nodes rather than subtrees. + */ + apportion: function (node, level) { + var firstChild = node.firstChild(), + firstChildLeftNeighbor = firstChild.leftNeighbor(), + compareDepth = 1, + depthToStop = this.CONFIG.maxDepth - level; + + while( firstChild && firstChildLeftNeighbor && compareDepth <= depthToStop ) { + // calculate the position of the firstChild, according to the position of firstChildLeftNeighbor + + var modifierSumRight = 0, + modifierSumLeft = 0, + leftAncestor = firstChildLeftNeighbor, + rightAncestor = firstChild; + + for ( var i = 0; i < compareDepth; i++ ) { + leftAncestor = leftAncestor.parent(); + rightAncestor = rightAncestor.parent(); + modifierSumLeft += leftAncestor.modifier; + modifierSumRight += rightAncestor.modifier; + + // all the stacked children are oriented towards right so use right variables + if ( rightAncestor.stackParent !== undefined ) { + modifierSumRight += rightAncestor.size() / 2; + } + } + + // find the gap between two trees and apply it to subTrees + // and mathing smaller gaps to smaller subtrees + + var totalGap = (firstChildLeftNeighbor.prelim + modifierSumLeft + firstChildLeftNeighbor.size() + this.CONFIG.subTeeSeparation) - (firstChild.prelim + modifierSumRight ); + + if ( totalGap > 0 ) { + var subtreeAux = node, + numSubtrees = 0; + + // count all the subtrees in the LeftSibling + while ( subtreeAux && subtreeAux.id !== leftAncestor.id ) { + subtreeAux = subtreeAux.leftSibling(); + numSubtrees++; + } + + if ( subtreeAux ) { + var subtreeMoveAux = node, + singleGap = totalGap / numSubtrees; + + while ( subtreeMoveAux.id !== leftAncestor.id ) { + subtreeMoveAux.prelim += totalGap; + subtreeMoveAux.modifier += totalGap; + + totalGap -= singleGap; + subtreeMoveAux = subtreeMoveAux.leftSibling(); + } + } + } + + compareDepth++; + + firstChild = ( firstChild.childrenCount() === 0 )? + node.leftMost(0, compareDepth): + firstChild = firstChild.firstChild(); + + if ( firstChild ) { + firstChildLeftNeighbor = firstChild.leftNeighbor(); + } + } + }, + + /* + * During a second pre-order walk, each node is given a + * final x-coordinate by summing its preliminary + * x-coordinate and the modifiers of all the node's + * ancestors. The y-coordinate depends on the height of + * the tree. (The roles of x and y are reversed for + * RootOrientations of EAST or WEST.) + */ + secondWalk: function( node, level, X, Y ) { + if ( level <= this.CONFIG.maxDepth ) { + var xTmp = node.prelim + X, + yTmp = Y, align = this.CONFIG.nodeAlign, + orient = this.CONFIG.rootOrientation, + levelHeight, nodesizeTmp; + + if (orient === 'NORTH' || orient === 'SOUTH') { + levelHeight = this.levelMaxDim[level].height; + nodesizeTmp = node.height; + if (node.pseudo) { + node.height = levelHeight; + } // assign a new size to pseudo nodes + } + else if (orient === 'WEST' || orient === 'EAST') { + levelHeight = this.levelMaxDim[level].width; + nodesizeTmp = node.width; + if (node.pseudo) { + node.width = levelHeight; + } // assign a new size to pseudo nodes + } + + node.X = xTmp; + + if (node.pseudo) { // pseudo nodes need to be properly aligned, otherwise position is not correct in some examples + if (orient === 'NORTH' || orient === 'WEST') { + node.Y = yTmp; // align "BOTTOM" + } + else if (orient === 'SOUTH' || orient === 'EAST') { + node.Y = (yTmp + (levelHeight - nodesizeTmp)); // align "TOP" + } + + } else { + node.Y = ( align === 'CENTER' ) ? (yTmp + (levelHeight - nodesizeTmp) / 2) : + ( align === 'TOP' ) ? (yTmp + (levelHeight - nodesizeTmp)) : + yTmp; + } + + if ( orient === 'WEST' || orient === 'EAST' ) { + var swapTmp = node.X; + node.X = node.Y; + node.Y = swapTmp; + } + + if (orient === 'SOUTH' ) { + node.Y = -node.Y - nodesizeTmp; + } + else if ( orient === 'EAST' ) { + node.X = -node.X - nodesizeTmp; + } + + if ( node.childrenCount() !== 0 ) { + if ( node.id === 0 && this.CONFIG.hideRootNode ) { + // ako je root node Hiden onda nemoj njegovu dijecu pomaknut po Y osi za Level separation, neka ona budu na vrhu + this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y); + } + else { + this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y + levelHeight + this.CONFIG.levelSeparation); + } + } + + if ( node.rightSibling() ) { + this.secondWalk( node.rightSibling(), level, X, Y ); + } + } + }, + + /** + * position all the nodes, center the tree in center of its container + * 0,0 coordinate is in the upper left corner + * @returns {Tree} + */ + positionNodes: function() { + var self = this, + treeSize = { + x: self.nodeDB.getMinMaxCoord('X', null, null), + y: self.nodeDB.getMinMaxCoord('Y', null, null) + }, + + treeWidth = treeSize.x.max - treeSize.x.min, + treeHeight = treeSize.y.max - treeSize.y.min, + + treeCenter = { + x: treeSize.x.max - treeWidth/2, + y: treeSize.y.max - treeHeight/2 + }; + + this.handleOverflow(treeWidth, treeHeight); + + var + containerCenter = { + x: self.drawArea.clientWidth/2, + y: self.drawArea.clientHeight/2 + }, + + deltaX = containerCenter.x - treeCenter.x, + deltaY = containerCenter.y - treeCenter.y, + + // all nodes must have positive X or Y coordinates, handle this with offsets + negOffsetX = ((treeSize.x.min + deltaX) <= 0) ? Math.abs(treeSize.x.min) : 0, + negOffsetY = ((treeSize.y.min + deltaY) <= 0) ? Math.abs(treeSize.y.min) : 0, + i, len, node; + + // position all the nodes + for ( i = 0, len = this.nodeDB.db.length; i < len; i++ ) { + + node = this.nodeDB.get(i); + + self.CONFIG.callback.onBeforePositionNode.apply( self, [node, i, containerCenter, treeCenter] ); + + if ( node.id === 0 && this.CONFIG.hideRootNode ) { + self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); + continue; + } + + // if the tree is smaller than the draw area, then center the tree within drawing area + node.X += negOffsetX + ((treeWidth < this.drawArea.clientWidth) ? deltaX : this.CONFIG.padding); + node.Y += negOffsetY + ((treeHeight < this.drawArea.clientHeight) ? deltaY : this.CONFIG.padding); + + var collapsedParent = node.collapsedParent(), + hidePoint = null; + + if (collapsedParent) { + // position the node behind the connector point of the parent, so future animations can be visible + hidePoint = collapsedParent.connectorPoint( true ); + node.hide(hidePoint); + + } + else if (node.positioned) { + // node is already positioned, + node.show(); + } + else { // inicijalno stvaranje nodeova, postavi lokaciju + node.nodeDOM.style.left = node.X + 'px'; + node.nodeDOM.style.top = node.Y + 'px'; + node.positioned = true; + } + + if (node.id !== 0 && !(node.parent().id === 0 && this.CONFIG.hideRootNode)) { + this.setConnectionToParent(node, hidePoint); // skip the root node + } + else if (!this.CONFIG.hideRootNode && node.drawLineThrough) { + // drawlinethrough is performed for for the root node also + node.drawLineThroughMe(); + } + + self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); + } + return this; + }, + + /** + * Create Raphael instance, (optionally set scroll bars if necessary) + * @param {number} treeWidth + * @param {number} treeHeight + * @returns {Tree} + */ + handleOverflow: function( treeWidth, treeHeight ) { + var viewWidth = (treeWidth < this.drawArea.clientWidth) ? this.drawArea.clientWidth : treeWidth + this.CONFIG.padding*2, + viewHeight = (treeHeight < this.drawArea.clientHeight) ? this.drawArea.clientHeight : treeHeight + this.CONFIG.padding*2; + + this._R.setSize( viewWidth, viewHeight ); + + if ( this.CONFIG.scrollbar === 'resize') { + UTIL.setDimensions( this.drawArea, viewWidth, viewHeight ); + } + else if ( !UTIL.isjQueryAvailable() || this.CONFIG.scrollbar === 'native' ) { + + if ( this.drawArea.clientWidth < treeWidth ) { // is overflow-x necessary + this.drawArea.style.overflowX = "auto"; + } + + if ( this.drawArea.clientHeight < treeHeight ) { // is overflow-y necessary + this.drawArea.style.overflowY = "auto"; + } + } + // Fancy scrollbar relies heavily on jQuery, so guarding with if ( $ ) + else if ( this.CONFIG.scrollbar === 'fancy') { + var jq_drawArea = $( this.drawArea ); + if (jq_drawArea.hasClass('ps-container')) { // znaci da je 'fancy' vec inicijaliziran, treba updateat + jq_drawArea.find('.Treant').css({ + width: viewWidth, + height: viewHeight + }); + + jq_drawArea.perfectScrollbar('update'); + } + else { + var mainContainer = jq_drawArea.wrapInner('
'), + child = mainContainer.find('.Treant'); + + child.css({ + width: viewWidth, + height: viewHeight + }); + + mainContainer.perfectScrollbar(); + } + } // else this.CONFIG.scrollbar == 'None' + + return this; + }, + /** + * @param {TreeNode} treeNode + * @param {boolean} hidePoint + * @returns {Tree} + */ + setConnectionToParent: function( treeNode, hidePoint ) { + var stacked = treeNode.stackParentId, + connLine, + parent = ( stacked? this.nodeDB.get( stacked ): treeNode.parent() ), + + pathString = hidePoint? + this.getPointPathString(hidePoint): + this.getPathString(parent, treeNode, stacked); + + if ( this.connectionStore[treeNode.id] ) { + // connector already exists, update the connector geometry + connLine = this.connectionStore[treeNode.id]; + this.animatePath( connLine, pathString ); + } + else { + connLine = this._R.path( pathString ); + this.connectionStore[treeNode.id] = connLine; + + // don't show connector arrows por pseudo nodes + if ( treeNode.pseudo ) { + delete parent.connStyle.style['arrow-end']; + } + if ( parent.pseudo ) { + delete parent.connStyle.style['arrow-start']; + } + + connLine.attr( parent.connStyle.style ); + + if ( treeNode.drawLineThrough || treeNode.pseudo ) { + treeNode.drawLineThroughMe( hidePoint ); + } + } + treeNode.connector = connLine; + return this; + }, + + /** + * Create the path which is represented as a point, used for hiding the connection + * A path with a leading "_" indicates the path will be hidden + * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path + * @param {object} hidePoint + * @returns {string} + */ + getPointPathString: function( hidePoint ) { + return ["_M", hidePoint.x, ",", hidePoint.y, 'L', hidePoint.x, ",", hidePoint.y, hidePoint.x, ",", hidePoint.y].join(' '); + }, + + /** + * This method relied on receiving a valid Raphael Paper.path. + * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path + * A pathString is typically in the format of "M10,20L30,40" + * @param path + * @param {string} pathString + * @returns {Tree} + */ + animatePath: function( path, pathString ) { + if (path.hidden && pathString.charAt(0) !== "_") { // path will be shown, so show it + path.show(); + path.hidden = false; + } + + // See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Element.animate + path.animate( + { + path: pathString.charAt(0) === "_"? + pathString.substring(1): + pathString // remove the "_" prefix if it exists + }, + this.CONFIG.animation.connectorsSpeed, + this.CONFIG.animation.connectorsAnimation, + function() { + if ( pathString.charAt(0) === "_" ) { // animation is hiding the path, hide it at the and of animation + path.hide(); + path.hidden = true; + } + } + ); + return this; + }, + + /** + * + * @param {TreeNode} from_node + * @param {TreeNode} to_node + * @param {boolean} stacked + * @returns {string} + */ + getPathString: function( from_node, to_node, stacked ) { + var startPoint = from_node.connectorPoint( true ), + endPoint = to_node.connectorPoint( false ), + orientation = this.CONFIG.rootOrientation, + connType = from_node.connStyle.type, + P1 = {}, P2 = {}; + + if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { + P1.y = P2.y = (startPoint.y + endPoint.y) / 2; + + P1.x = startPoint.x; + P2.x = endPoint.x; + } + else if ( orientation === 'EAST' || orientation === 'WEST' ) { + P1.x = P2.x = (startPoint.x + endPoint.x) / 2; + + P1.y = startPoint.y; + P2.y = endPoint.y; + } + + // sp, p1, pm, p2, ep == "x,y" + var sp = startPoint.x+','+startPoint.y, p1 = P1.x+','+P1.y, p2 = P2.x+','+P2.y, ep = endPoint.x+','+endPoint.y, + pm = (P1.x + P2.x)/2 +','+ (P1.y + P2.y)/2, pathString, stackPoint; + + if ( stacked ) { // STACKED CHILDREN + + stackPoint = (orientation === 'EAST' || orientation === 'WEST')? + endPoint.x+','+startPoint.y: + startPoint.x+','+endPoint.y; + + if ( connType === "step" || connType === "straight" ) { + pathString = ["M", sp, 'L', stackPoint, 'L', ep]; + } + else if ( connType === "curve" || connType === "bCurve" ) { + var helpPoint, // used for nicer curve lines + indent = from_node.connStyle.stackIndent; + + if ( orientation === 'NORTH' ) { + helpPoint = (endPoint.x - indent)+','+(endPoint.y - indent); + } + else if ( orientation === 'SOUTH' ) { + helpPoint = (endPoint.x - indent)+','+(endPoint.y + indent); + } + else if ( orientation === 'EAST' ) { + helpPoint = (endPoint.x + indent) +','+startPoint.y; + } + else if ( orientation === 'WEST' ) { + helpPoint = (endPoint.x - indent) +','+startPoint.y; + } + pathString = ["M", sp, 'L', helpPoint, 'S', stackPoint, ep]; + } + + } + else { // NORMAL CHILDREN + if ( connType === "step" ) { + pathString = ["M", sp, 'L', p1, 'L', p2, 'L', ep]; + } + else if ( connType === "curve" ) { + pathString = ["M", sp, 'C', p1, p2, ep ]; + } + else if ( connType === "bCurve" ) { + pathString = ["M", sp, 'Q', p1, pm, 'T', ep]; + } + else if (connType === "straight" ) { + pathString = ["M", sp, 'L', sp, ep]; + } + } + + return pathString.join(" "); + }, + + /** + * Algorithm works from left to right, so previous processed node will be left neighbour of the next node + * @param {TreeNode} node + * @param {number} level + * @returns {Tree} + */ + setNeighbors: function( node, level ) { + node.leftNeighborId = this.lastNodeOnLevel[level]; + if ( node.leftNeighborId ) { + node.leftNeighbor().rightNeighborId = node.id; + } + this.lastNodeOnLevel[level] = node.id; + return this; + }, + + /** + * Used for calculation of height and width of a level (level dimensions) + * @param {TreeNode} node + * @param {number} level + * @returns {Tree} + */ + calcLevelDim: function( node, level ) { // root node is on level 0 + this.levelMaxDim[level] = { + width: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].width: 0, node.width ), + height: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].height: 0, node.height ) + }; + return this; + }, + + /** + * @returns {Tree} + */ + resetLevelData: function() { + this.lastNodeOnLevel = []; + this.levelMaxDim = []; + return this; + }, + + /** + * @returns {TreeNode} + */ + root: function() { + return this.nodeDB.get( 0 ); + } + }; + + /** + * NodeDB is used for storing the nodes. Each tree has its own NodeDB. + * @param {object} nodeStructure + * @param {Tree} tree + * @constructor + */ + var NodeDB = function ( nodeStructure, tree ) { + this.reset( nodeStructure, tree ); + }; + + NodeDB.prototype = { + + /** + * @param {object} nodeStructure + * @param {Tree} tree + * @returns {NodeDB} + */ + reset: function( nodeStructure, tree ) { + + this.db = []; + + var self = this; + + /** + * @param {object} node + * @param {number} parentId + */ + function iterateChildren( node, parentId ) { + var newNode = self.createNode( node, parentId, tree, null ); + + if ( node.children ) { + // pseudo node is used for descending children to the next level + if ( node.childrenDropLevel && node.childrenDropLevel > 0 ) { + while ( node.childrenDropLevel-- ) { + // pseudo node needs to inherit the connection style from its parent for continuous connectors + var connStyle = UTIL.cloneObj( newNode.connStyle ); + newNode = self.createNode( 'pseudo', newNode.id, tree, null ); + newNode.connStyle = connStyle; + newNode.children = []; + } + } + + var stack = ( node.stackChildren && !self.hasGrandChildren( node ) )? newNode.id: null; + + // children are positioned on separate levels, one beneath the other + if ( stack !== null ) { + newNode.stackChildren = []; + } + + for ( var i = 0, len = node.children.length; i < len ; i++ ) { + if ( stack !== null ) { + newNode = self.createNode( node.children[i], newNode.id, tree, stack ); + if ( ( i + 1 ) < len ) { + // last node cant have children + newNode.children = []; + } + } + else { + iterateChildren( node.children[i], newNode.id ); + } + } + } + } + + if ( tree.CONFIG.animateOnInit ) { + nodeStructure.collapsed = true; + } + + iterateChildren( nodeStructure, -1 ); // root node + + this.createGeometries( tree ); + + return this; + }, + + /** + * @param {Tree} tree + * @returns {NodeDB} + */ + createGeometries: function( tree ) { + var i = this.db.length; + + while ( i-- ) { + this.get( i ).createGeometry( tree ); + } + return this; + }, + + /** + * @param {number} nodeId + * @returns {TreeNode} + */ + get: function ( nodeId ) { + return this.db[nodeId]; // get TreeNode by ID + }, + + /** + * @param {function} callback + * @returns {NodeDB} + */ + walk: function( callback ) { + var i = this.db.length; + + while ( i-- ) { + callback.apply( this, [ this.get( i ) ] ); + } + return this; + }, + + /** + * + * @param {object} nodeStructure + * @param {number} parentId + * @param {Tree} tree + * @param {number} stackParentId + * @returns {TreeNode} + */ + createNode: function( nodeStructure, parentId, tree, stackParentId ) { + var node = new TreeNode( nodeStructure, this.db.length, parentId, tree, stackParentId ); + + this.db.push( node ); + + // skip root node (0) + if ( parentId >= 0 ) { + var parent = this.get( parentId ); + + // todo: refactor into separate private method + if ( nodeStructure.position ) { + if ( nodeStructure.position === 'left' ) { + parent.children.push( node.id ); + } + else if ( nodeStructure.position === 'right' ) { + parent.children.splice( 0, 0, node.id ); + } + else if ( nodeStructure.position === 'center' ) { + parent.children.splice( Math.floor( parent.children.length / 2 ), 0, node.id ); + } + else { + // edge case when there's only 1 child + var position = parseInt( nodeStructure.position ); + if ( parent.children.length === 1 && position > 0 ) { + parent.children.splice( 0, 0, node.id ); + } + else { + parent.children.splice( + Math.max( position, parent.children.length - 1 ), + 0, node.id + ); + } + } + } + else { + parent.children.push( node.id ); + } + } + + if ( stackParentId ) { + this.get( stackParentId ).stackParent = true; + this.get( stackParentId ).stackChildren.push( node.id ); + } + + return node; + }, + + getMinMaxCoord: function( dim, parent, MinMax ) { // used for getting the dimensions of the tree, dim = 'X' || 'Y' + // looks for min and max (X and Y) within the set of nodes + parent = parent || this.get(0); + + MinMax = MinMax || { // start with root node dimensions + min: parent[dim], + max: parent[dim] + ( ( dim === 'X' )? parent.width: parent.height ) + }; + + var i = parent.childrenCount(); + + while ( i-- ) { + var node = parent.childAt( i ), + maxTest = node[dim] + ( ( dim === 'X' )? node.width: node.height ), + minTest = node[dim]; + + if ( maxTest > MinMax.max ) { + MinMax.max = maxTest; + } + if ( minTest < MinMax.min ) { + MinMax.min = minTest; + } + + this.getMinMaxCoord( dim, node, MinMax ); + } + return MinMax; + }, + + /** + * @param {object} nodeStructure + * @returns {boolean} + */ + hasGrandChildren: function( nodeStructure ) { + var i = nodeStructure.children.length; + while ( i-- ) { + if ( nodeStructure.children[i].children ) { + return true; + } + } + return false; + } + }; + + /** + * TreeNode constructor. + * @param {object} nodeStructure + * @param {number} id + * @param {number} parentId + * @param {Tree} tree + * @param {number} stackParentId + * @constructor + */ + var TreeNode = function( nodeStructure, id, parentId, tree, stackParentId ) { + this.reset( nodeStructure, id, parentId, tree, stackParentId ); + }; + + TreeNode.prototype = { + + /** + * @param {object} nodeStructure + * @param {number} id + * @param {number} parentId + * @param {Tree} tree + * @param {number} stackParentId + * @returns {TreeNode} + */ + reset: function( nodeStructure, id, parentId, tree, stackParentId ) { + this.id = id; + this.parentId = parentId; + this.treeId = tree.id; + + this.prelim = 0; + this.modifier = 0; + this.leftNeighborId = null; + + this.stackParentId = stackParentId; + + // pseudo node is a node with width=height=0, it is invisible, but necessary for the correct positioning of the tree + this.pseudo = nodeStructure === 'pseudo' || nodeStructure['pseudo']; // todo: surely if nodeStructure is a scalar then the rest will error: + + this.meta = nodeStructure.meta || {}; + this.image = nodeStructure.image || null; + + this.link = UTIL.createMerge( tree.CONFIG.node.link, nodeStructure.link ); + + this.connStyle = UTIL.createMerge( tree.CONFIG.connectors, nodeStructure.connectors ); + this.connector = null; + + this.drawLineThrough = nodeStructure.drawLineThrough === false ? false : ( nodeStructure.drawLineThrough || tree.CONFIG.node.drawLineThrough ); + + this.collapsable = nodeStructure.collapsable === false ? false : ( nodeStructure.collapsable || tree.CONFIG.node.collapsable ); + this.collapsed = nodeStructure.collapsed; + + this.text = nodeStructure.text; + + // '.node' DIV + this.nodeInnerHTML = nodeStructure.innerHTML; + this.nodeHTMLclass = (tree.CONFIG.node.HTMLclass ? tree.CONFIG.node.HTMLclass : '') + // globally defined class for the nodex + (nodeStructure.HTMLclass ? (' ' + nodeStructure.HTMLclass) : ''); // + specific node class + + this.nodeHTMLid = nodeStructure.HTMLid; + + this.children = []; + + return this; + }, + + /** + * @returns {Tree} + */ + getTree: function() { + return TreeStore.get( this.treeId ); + }, + + /** + * @returns {object} + */ + getTreeConfig: function() { + return this.getTree().CONFIG; + }, + + /** + * @returns {NodeDB} + */ + getTreeNodeDb: function() { + return this.getTree().getNodeDb(); + }, + + /** + * @param {number} nodeId + * @returns {TreeNode} + */ + lookupNode: function( nodeId ) { + return this.getTreeNodeDb().get( nodeId ); + }, + + /** + * @returns {Tree} + */ + Tree: function() { + return TreeStore.get( this.treeId ); + }, + + /** + * @param {number} nodeId + * @returns {TreeNode} + */ + dbGet: function( nodeId ) { + return this.getTreeNodeDb().get( nodeId ); + }, + + /** + * Returns the width of the node + * @returns {float} + */ + size: function() { + var orientation = this.getTreeConfig().rootOrientation; + + if ( this.pseudo ) { + // prevents separating the subtrees + return ( -this.getTreeConfig().subTeeSeparation ); + } + + if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { + return this.width; + } + else if ( orientation === 'WEST' || orientation === 'EAST' ) { + return this.height; + } + }, + + /** + * @returns {number} + */ + childrenCount: function () { + return ( ( this.collapsed || !this.children)? 0: this.children.length ); + }, + + /** + * @param {number} index + * @returns {TreeNode} + */ + childAt: function( index ) { + return this.dbGet( this.children[index] ); + }, + + /** + * @returns {TreeNode} + */ + firstChild: function() { + return this.childAt( 0 ); + }, + + /** + * @returns {TreeNode} + */ + lastChild: function() { + return this.childAt( this.children.length - 1 ); + }, + + /** + * @returns {TreeNode} + */ + parent: function() { + return this.lookupNode( this.parentId ); + }, + + /** + * @returns {TreeNode} + */ + leftNeighbor: function() { + if ( this.leftNeighborId ) { + return this.lookupNode( this.leftNeighborId ); + } + }, + + /** + * @returns {TreeNode} + */ + rightNeighbor: function() { + if ( this.rightNeighborId ) { + return this.lookupNode( this.rightNeighborId ); + } + }, + + /** + * @returns {TreeNode} + */ + leftSibling: function () { + var leftNeighbor = this.leftNeighbor(); + + if ( leftNeighbor && leftNeighbor.parentId === this.parentId ){ + return leftNeighbor; + } + }, + + /** + * @returns {TreeNode} + */ + rightSibling: function () { + var rightNeighbor = this.rightNeighbor(); + + if ( rightNeighbor && rightNeighbor.parentId === this.parentId ) { + return rightNeighbor; + } + }, + + /** + * @returns {number} + */ + childrenCenter: function () { + var first = this.firstChild(), + last = this.lastChild(); + + return ( first.prelim + ((last.prelim - first.prelim) + last.size()) / 2 ); + }, + + /** + * Find out if one of the node ancestors is collapsed + * @returns {*} + */ + collapsedParent: function() { + var parent = this.parent(); + if ( !parent ) { + return false; + } + if ( parent.collapsed ) { + return parent; + } + return parent.collapsedParent(); + }, + + /** + * Returns the leftmost child at specific level, (initial level = 0) + * @param level + * @param depth + * @returns {*} + */ + leftMost: function ( level, depth ) { + if ( level >= depth ) { + return this; + } + if ( this.childrenCount() === 0 ) { + return; + } + + for ( var i = 0, n = this.childrenCount(); i < n; i++ ) { + var leftmostDescendant = this.childAt( i ).leftMost( level + 1, depth ); + if ( leftmostDescendant ) { + return leftmostDescendant; + } + } + }, + + // returns start or the end point of the connector line, origin is upper-left + connectorPoint: function(startPoint) { + var orient = this.Tree().CONFIG.rootOrientation, point = {}; + + if ( this.stackParentId ) { // return different end point if node is a stacked child + if ( orient === 'NORTH' || orient === 'SOUTH' ) { + orient = 'WEST'; + } + else if ( orient === 'EAST' || orient === 'WEST' ) { + orient = 'NORTH'; + } + } + + // if pseudo, a virtual center is used + if ( orient === 'NORTH' ) { + point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; + point.y = (startPoint) ? this.Y + this.height : this.Y; + } + else if (orient === 'SOUTH') { + point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; + point.y = (startPoint) ? this.Y : this.Y + this.height; + } + else if (orient === 'EAST') { + point.x = (startPoint) ? this.X : this.X + this.width; + point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; + } + else if (orient === 'WEST') { + point.x = (startPoint) ? this.X + this.width : this.X; + point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; + } + return point; + }, + + /** + * @returns {string} + */ + pathStringThrough: function() { // get the geometry of a path going through the node + var startPoint = this.connectorPoint( true ), + endPoint = this.connectorPoint( false ); + + return ["M", startPoint.x+","+startPoint.y, 'L', endPoint.x+","+endPoint.y].join(" "); + }, + + /** + * @param {object} hidePoint + */ + drawLineThroughMe: function( hidePoint ) { // hidepoint se proslijedjuje ako je node sakriven zbog collapsed + var pathString = hidePoint? + this.Tree().getPointPathString( hidePoint ): + this.pathStringThrough(); + + this.lineThroughMe = this.lineThroughMe || this.Tree()._R.path(pathString); + + var line_style = UTIL.cloneObj( this.connStyle.style ); + + delete line_style['arrow-start']; + delete line_style['arrow-end']; + + this.lineThroughMe.attr( line_style ); + + if ( hidePoint ) { + this.lineThroughMe.hide(); + this.lineThroughMe.hidden = true; + } + }, + + addSwitchEvent: function( nodeSwitch ) { + var self = this; + UTIL.addEvent( nodeSwitch, 'click', + function( e ) { + e.preventDefault(); + if ( self.getTreeConfig().callback.onBeforeClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ) === false ) { + return false; + } + + self.toggleCollapse(); + + self.getTreeConfig().callback.onAfterClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ); + } + ); + }, + + /** + * @returns {TreeNode} + */ + collapse: function() { + if ( !this.collapsed ) { + this.toggleCollapse(); + } + return this; + }, + + /** + * @returns {TreeNode} + */ + expand: function() { + if ( this.collapsed ) { + this.toggleCollapse(); + } + return this; + }, + + /** + * @returns {TreeNode} + */ + toggleCollapse: function() { + var oTree = this.getTree(); + + if ( !oTree.inAnimation ) { + oTree.inAnimation = true; + + this.collapsed = !this.collapsed; // toggle the collapse at each click + UTIL.toggleClass( this.nodeDOM, 'collapsed', this.collapsed ); + + oTree.positionTree(); + + var self = this; + + setTimeout( + function() { // set the flag after the animation + oTree.inAnimation = false; + oTree.CONFIG.callback.onToggleCollapseFinished.apply( oTree, [ self, self.collapsed ] ); + }, + ( oTree.CONFIG.animation.nodeSpeed > oTree.CONFIG.animation.connectorsSpeed )? + oTree.CONFIG.animation.nodeSpeed: + oTree.CONFIG.animation.connectorsSpeed + ); + } + return this; + }, + + hide: function( collapse_to_point ) { + collapse_to_point = collapse_to_point || false; + + var bCurrentState = this.hidden; + this.hidden = true; + + this.nodeDOM.style.overflow = 'hidden'; + + var tree = this.getTree(), + config = this.getTreeConfig(), + oNewState = { + opacity: 0 + }; + + if ( collapse_to_point ) { + oNewState.left = collapse_to_point.x; + oNewState.top = collapse_to_point.y; + } + + // if parent was hidden in initial configuration, position the node behind the parent without animations + if ( !this.positioned || bCurrentState ) { + this.nodeDOM.style.visibility = 'hidden'; + if ( $ ) { + $( this.nodeDOM ).css( oNewState ); + } + else { + this.nodeDOM.style.left = oNewState.left + 'px'; + this.nodeDOM.style.top = oNewState.top + 'px'; + } + this.positioned = true; + } + else { + // todo: fix flashy bug when a node is manually hidden and tree.redraw is called. + if ( $ ) { + $( this.nodeDOM ).animate( + oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation, + function () { + this.style.visibility = 'hidden'; + } + ); + } + else { + this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; + this.nodeDOM.style.transitionProperty = 'opacity, left, top'; + this.nodeDOM.style.opacity = oNewState.opacity; + this.nodeDOM.style.left = oNewState.left + 'px'; + this.nodeDOM.style.top = oNewState.top + 'px'; + this.nodeDOM.style.visibility = 'hidden'; + } + } + + // animate the line through node if the line exists + if ( this.lineThroughMe ) { + var new_path = tree.getPointPathString( collapse_to_point ); + if ( bCurrentState ) { + // update without animations + this.lineThroughMe.attr( { path: new_path } ); + } + else { + // update with animations + tree.animatePath( this.lineThroughMe, tree.getPointPathString( collapse_to_point ) ); + } + } + + return this; + }, + + /** + * @returns {TreeNode} + */ + hideConnector: function() { + var oTree = this.Tree(); + var oPath = oTree.connectionStore[this.id]; + if ( oPath ) { + oPath.animate( + { 'opacity': 0 }, + oTree.CONFIG.animation.connectorsSpeed, + oTree.CONFIG.animation.connectorsAnimation + ); + } + return this; + }, + + show: function() { + var bCurrentState = this.hidden; + this.hidden = false; + + this.nodeDOM.style.visibility = 'visible'; + + var oTree = this.Tree(); + + var oNewState = { + left: this.X, + top: this.Y, + opacity: 1 + }, + config = this.getTreeConfig(); + + // if the node was hidden, update opacity and position + if ( $ ) { + $( this.nodeDOM ).animate( + oNewState, + config.animation.nodeSpeed, config.animation.nodeAnimation, + function () { + // $.animate applies "overflow:hidden" to the node, remove it to avoid visual problems + this.style.overflow = ""; + } + ); + } + else { + this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; + this.nodeDOM.style.transitionProperty = 'opacity, left, top'; + this.nodeDOM.style.left = oNewState.left + 'px'; + this.nodeDOM.style.top = oNewState.top + 'px'; + this.nodeDOM.style.opacity = oNewState.opacity; + this.nodeDOM.style.overflow = ''; + } + + if ( this.lineThroughMe ) { + this.getTree().animatePath( this.lineThroughMe, this.pathStringThrough() ); + } + + return this; + }, + + /** + * @returns {TreeNode} + */ + showConnector: function() { + var oTree = this.Tree(); + var oPath = oTree.connectionStore[this.id]; + if ( oPath ) { + oPath.animate( + { 'opacity': 1 }, + oTree.CONFIG.animation.connectorsSpeed, + oTree.CONFIG.animation.connectorsAnimation + ); + } + return this; + } + }; + + + /** + * Build a node from the 'text' and 'img' property and return with it. + * + * The node will contain all the fields that present under the 'text' property + * Each field will refer to a css class with name defined as node-{$property_name} + * + * Example: + * The definition: + * + * text: { + * desc: "some description", + * paragraph: "some text" + * } + * + * will generate the following elements: + * + *some description
+ *some text
+ * + * @Returns the configured node + */ + TreeNode.prototype.buildNodeFromText = function (node) { + // IMAGE + if (this.image) { + image = document.createElement('img'); + image.src = this.image; + node.appendChild(image); + } + + // TEXT + if (this.text) { + for (var key in this.text) { + // adding DATA Attributes to the node + if (key.startsWith("data-")) { + node.setAttribute(key, this.text[key]); + } else { + + var textElement = document.createElement(this.text[key].href ? 'a' : 'p'); + + // make an element if required + if (this.text[key].href) { + textElement.href = this.text[key].href; + if (this.text[key].target) { + textElement.target = this.text[key].target; + } + } + + textElement.className = "node-"+key; + textElement.appendChild(document.createTextNode( + this.text[key].val ? this.text[key].val : + this.text[key] instanceof Object ? "'val' param missing!" : this.text[key] + ) + ); + + node.appendChild(textElement); + } + } + } + return node; + }; + + /** + * Build a node from 'nodeInnerHTML' property that defines an existing HTML element, referenced by it's id, e.g: #someElement + * Change the text in the passed node to 'Wrong ID selector' if the referenced element does ot exist, + * return with a cloned and configured node otherwise + * + * @Returns node the configured node + */ + TreeNode.prototype.buildNodeFromHtml = function(node) { + // get some element by ID and clone its structure into a node + if (this.nodeInnerHTML.charAt(0) === "#") { + var elem = document.getElementById(this.nodeInnerHTML.substring(1)); + if (elem) { + node = elem.cloneNode(true); + node.id += "-clone"; + node.className += " node"; + } + else { + node.innerHTML = " Wrong ID selector "; + } + } + else { + // insert your custom HTML into a node + node.innerHTML = this.nodeInnerHTML; + } + return node; + }; + + /** + * @param {Tree} tree + */ + TreeNode.prototype.createGeometry = function( tree ) { + if ( this.id === 0 && tree.CONFIG.hideRootNode ) { + this.width = 0; + this.height = 0; + return; + } + + var drawArea = tree.drawArea, + image, + + /////////// CREATE NODE ////////////// + node = document.createElement( this.link.href? 'a': 'div' ); + + node.className = ( !this.pseudo )? TreeNode.CONFIG.nodeHTMLclass: 'pseudo'; + if ( this.nodeHTMLclass && !this.pseudo ) { + node.className += ' ' + this.nodeHTMLclass; + } + + if ( this.nodeHTMLid ) { + node.id = this.nodeHTMLid; + } + + if ( this.link.href ) { + node.href = this.link.href; + node.target = this.link.target; + } + + if ( $ ) { + $( node ).data( 'treenode', this ); + } + else { + node.data = { + 'treenode': this + }; + } + + /////////// BUILD NODE CONTENT ////////////// + if ( !this.pseudo ) { + node = this.nodeInnerHTML? this.buildNodeFromHtml(node) : this.buildNodeFromText(node) + + // handle collapse switch + if ( this.collapsed || (this.collapsable && this.childrenCount() && !this.stackParentId) ) { + this.createSwitchGeometry( tree, node ); + } + } + + tree.CONFIG.callback.onCreateNode.apply( tree, [this, node] ); + + /////////// APPEND all ////////////// + drawArea.appendChild(node); + + this.width = node.offsetWidth; + this.height = node.offsetHeight; + + this.nodeDOM = node; + + tree.imageLoader.processNode(this); + }; + + /** + * @param {Tree} tree + * @param {Element} nodeEl + */ + TreeNode.prototype.createSwitchGeometry = function( tree, nodeEl ) { + nodeEl = nodeEl || this.nodeDOM; + + // safe guard and check to see if it has a collapse switch + var nodeSwitchEl = UTIL.findEl( '.collapse-switch', true, nodeEl ); + if ( !nodeSwitchEl ) { + nodeSwitchEl = document.createElement( 'a' ); + nodeSwitchEl.className = "collapse-switch"; + + nodeEl.appendChild( nodeSwitchEl ); + this.addSwitchEvent( nodeSwitchEl ); + if ( this.collapsed ) { + nodeEl.className += " collapsed"; + } + + tree.CONFIG.callback.onCreateNodeCollapseSwitch.apply( tree, [this, nodeEl, nodeSwitchEl] ); + } + return nodeSwitchEl; + }; + + + // ########################################### + // Expose global + default CONFIG params + // ########################################### + + + Tree.CONFIG = { + maxDepth: 100, + rootOrientation: 'NORTH', // NORTH || EAST || WEST || SOUTH + nodeAlign: 'CENTER', // CENTER || TOP || BOTTOM + levelSeparation: 30, + siblingSeparation: 30, + subTeeSeparation: 30, + + hideRootNode: false, + + animateOnInit: false, + animateOnInitDelay: 500, + + padding: 15, // the difference is seen only when the scrollbar is shown + scrollbar: 'native', // "native" || "fancy" || "None" (PS: "fancy" requires jquery and perfect-scrollbar) + + connectors: { + type: 'curve', // 'curve' || 'step' || 'straight' || 'bCurve' + style: { + stroke: 'black' + }, + stackIndent: 15 + }, + + node: { // each node inherits this, it can all be overridden in node config + + // HTMLclass: 'node', + // drawLineThrough: false, + // collapsable: false, + link: { + target: '_self' + } + }, + + animation: { // each node inherits this, it can all be overridden in node config + nodeSpeed: 450, + nodeAnimation: 'linear', + connectorsSpeed: 450, + connectorsAnimation: 'linear' + }, + + callback: { + onCreateNode: function( treeNode, treeNodeDom ) {}, // this = Tree + onCreateNodeCollapseSwitch: function( treeNode, treeNodeDom, switchDom ) {}, // this = Tree + onAfterAddNode: function( newTreeNode, parentTreeNode, nodeStructure ) {}, // this = Tree + onBeforeAddNode: function( parentTreeNode, nodeStructure ) {}, // this = Tree + onAfterPositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree + onBeforePositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree + onToggleCollapseFinished: function ( treeNode, bIsCollapsed ) {}, // this = Tree + onAfterClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode + onBeforeClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode + onTreeLoaded: function( rootTreeNode ) {} // this = Tree + } + }; + + TreeNode.CONFIG = { + nodeHTMLclass: 'node' + }; + + // ############################################# + // Makes a JSON chart config out of Array config + // ############################################# + + var JSONconfig = { + make: function( configArray ) { + + var i = configArray.length, node; + + this.jsonStructure = { + chart: null, + nodeStructure: null + }; + //fist loop: find config, find root; + while(i--) { + node = configArray[i]; + if (node.hasOwnProperty('container')) { + this.jsonStructure.chart = node; + continue; + } + + if (!node.hasOwnProperty('parent') && ! node.hasOwnProperty('container')) { + this.jsonStructure.nodeStructure = node; + node._json_id = 0; + } + } + + this.findChildren(configArray); + + return this.jsonStructure; + }, + + findChildren: function(nodes) { + var parents = [0]; // start with a a root node + + while(parents.length) { + var parentId = parents.pop(), + parent = this.findNode(this.jsonStructure.nodeStructure, parentId), + i = 0, len = nodes.length, + children = []; + + for(;i>>0>=t>>>0){var ka=z;break a}Y=z}}else{ka=y}}while(0);15==(ka|0)&&(c[P]=1);g=r=g+1;if(128<=r>>>0){c[da]=2;break}r=c[S];c[da]=r;if(0!=(r|0)){break}}c[a+93]=a+9*c[a+92]+37;g=c[a+94];0==(g|0)?k[a+36]=JB(a+32):1==(g|0)&&(k[a+36]=0);a=c[a+94];b=f;return a}O8a.X=1;function a0(a,d,e,f){0!=(f|0)?V8a(a,d,e):W8a(a,d,e)}a0.X=1;function X8a(a,d,e,f,g,h,i){var j=b;b+=3006;var l,m=j+32,n=j+127,p=j+131,r=j+2967,s=j+2971,t=j+2975,w=j+2976,x=j+2977,y=j+2978,z=j+2982,A=j+2986,C=j+2990,B=j+2994,K=j+2998,E=j+3002;M8a(a,d,e,f,h,j,i&1);N8a(m);WP(n,g);a=O8a(m,j,n);do{if(1==(a|0)){if(Y8a(p),WP(r,g),9!=(Z8a(p,m,r)|0)){k[t]=0;k[w]=0;k[x]=0;H(s,t,w,x);l=0;e=p+9;f=l>>>0 =C&&(n=(r?taa:saa)(n&Math.pow(256,C)-1,8*C));var K=Math.abs(n),r="";if(100==p||105==p){B=taa(n,8*C).toString(10)}else{if(117==p){B=saa(n,8*C).toString(10),n=Math.abs(n)}else{if(111==p){B=(w?"0":"")+K.toString(8)}else{if(120==p||88==p){r=w?"0x":"";if(0>n){n=-n;B=(K-1).toString(16);K=[];for(w=0;w i?0:e h&&(h=-F*g,h>k[c[d+7]+x]&&(k[c[d+7]+x]=h)):0=h){c[a+15]=8;break a}}while(0);h=P;A=m;P=ba;c[A]=c[P];k[A]=k[P];c[A+1]=c[P+1];k[A+1]=k[P+1];c[A+2]=c[P+2];k[A+2]=k[P+2];c[A+3]=c[P+3];k[A+3]=k[P+3];A=n;P=$;c[A]=c[P];k[A]=k[P];c[A+1]=c[P+1];k[A+1]=k[P+1];c[A+2]=c[P+2];k[A+2]=k[P+2];c[A+3]=c[P+3];k[A+3]=k[P+3];A=g;P=Y;c[A]=c[P];k[A]=k[P];c[A+1]=c[P+1];k[A+1]=k[P+1];c[A+2]=c[P+2];k[A+2]=k[P+2];c[A+3]=c[P+3];k[A+3]=k[P+3];A=1;c[a+15]=3}else{c[a+15]=9}}else{if(0>>15)+14,j=(j<<1)+(a>>>(j+7>>>0)&1));i=s=c[f+(j+76)];s=0!=(s|0);do{if(s){d=a<<(31==(j|0)?0:-(j>>>1)+25);for(l=0;;){n=(c[i+1]&-8)-a;if(n>>>0