diff --git a/assets/icons/edges.svg b/assets/icons/edges.svg new file mode 100644 index 0000000..4b4eec6 --- /dev/null +++ b/assets/icons/edges.svg @@ -0,0 +1,90 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/source/viewer/domutils.js b/source/viewer/domutils.js index afa8a41..8a64d87 100644 --- a/source/viewer/domutils.js +++ b/source/viewer/domutils.js @@ -115,6 +115,27 @@ OV.SetDomElementOuterHeight = function (element, height) OV.SetDomElementHeight (element, height - OV.GetDomElementExternalHeight (style)); }; +OV.AddRadioButton = function (parentElement, name, id, text, onChange) +{ + let label = OV.AddDomElement (parentElement, 'label'); + label.setAttribute ('for', id); + let radio = OV.AddDomElement (label, 'input', 'ov_radio_button'); + radio.setAttribute ('type', 'radio'); + radio.setAttribute ('id', id); + radio.setAttribute ('name', name); + OV.AddDomElement (label, 'span', null, text); + radio.addEventListener ('change', onChange); + return radio; +}; + +OV.SelectRadioButton = function (radioButtons, selectedId) +{ + for (let radioButton of radioButtons) { + let isChecked = radioButton.getAttribute ('id') === selectedId; + radioButton.checked = isChecked; + } +}; + OV.CreateDiv = function (className, innerHTML) { return OV.CreateDomElement ('div', className, innerHTML); diff --git a/source/viewer/viewer.js b/source/viewer/viewer.js index b6ac7f3..223ce7e 100644 --- a/source/viewer/viewer.js +++ b/source/viewer/viewer.js @@ -35,6 +35,23 @@ OV.TraverseThreeObject = function (object, processor) return true; }; +OV.SetThreeMeshPolygonOffset = function (mesh, offset) +{ + function SetMaterialsPolygonOffset (materials, offset) + { + for (let material of materials) { + material.polygonOffset = offset; + material.polygonOffsetUnit = 1; + material.polygonOffsetFactor = 1; + } + } + + SetMaterialsPolygonOffset (mesh.material, offset); + if (mesh.userData.threeMaterials) { + SetMaterialsPolygonOffset (mesh.userData.threeMaterials, offset); + } +}; + OV.GetShadingTypeOfObject = function (mainObject) { let shadingType = null; @@ -112,23 +129,112 @@ OV.ViewerGeometry = class { this.scene = scene; this.mainObject = null; + this.mainEdgeObject = null; + this.edgeSettings = { + showEdges : false, + edgeColor : new OV.Color (0, 0, 0), + edgeThreshold : 1 + }; } SetMainObject (mainObject) { this.mainObject = mainObject; this.scene.add (this.mainObject); + if (this.edgeSettings.showEdges) { + this.GenerateMainEdgeObject (); + } + } + + SetEdgeSettings (show, color, threshold) + { + let needToGenerate = false; + if (show && (!this.edgeSettings.showEdges || this.edgeSettings.edgeThreshold !== threshold)) { + needToGenerate = true; + } + + this.edgeSettings.showEdges = show; + this.edgeSettings.edgeThreshold = threshold; + this.edgeSettings.edgeColor = color; + + if (this.mainObject === null) { + return; + } + + if (this.edgeSettings.showEdges) { + if (needToGenerate) { + this.ClearMainEdgeObject (); + this.GenerateMainEdgeObject (); + } else { + let edgeColor = new THREE.Color ( + this.edgeSettings.edgeColor.r / 255.0, + this.edgeSettings.edgeColor.g / 255.0, + this.edgeSettings.edgeColor.b / 255.0 + ); + this.EnumerateEdges ((edge) => { + edge.material.color = edgeColor; + }); + } + } else { + this.ClearMainEdgeObject (); + } + } + + GenerateMainEdgeObject () + { + let edgeColor = new THREE.Color ( + this.edgeSettings.edgeColor.r / 255.0, + this.edgeSettings.edgeColor.g / 255.0, + this.edgeSettings.edgeColor.b / 255.0 + ); + this.mainEdgeObject = new THREE.Object3D (); + this.mainObject.updateWorldMatrix (true, true); + this.EnumerateMeshes ((mesh) => { + OV.SetThreeMeshPolygonOffset (mesh, true); + let edges = new THREE.EdgesGeometry (mesh.geometry, this.edgeSettings.edgeThreshold); + let line = new THREE.LineSegments (edges, new THREE.LineBasicMaterial ({ + color: edgeColor + })); + line.applyMatrix4 (mesh.matrixWorld); + line.userData = mesh.userData; + this.mainEdgeObject.add (line); + }); + this.scene.add (this.mainEdgeObject); + } + + Clear () + { + this.ClearMainObject (); + this.ClearMainEdgeObject (); } ClearMainObject () { - if (this.mainObject !== null) { - this.EnumerateMeshes ((mesh) => { - mesh.geometry.dispose (); - }); - this.scene.remove (this.mainObject); - this.mainObject = null; + if (this.mainObject === null) { + return; } + + this.EnumerateMeshes ((mesh) => { + mesh.geometry.dispose (); + }); + this.scene.remove (this.mainObject); + this.mainObject = null; + } + + ClearMainEdgeObject () + { + if (this.mainEdgeObject === null) { + return; + } + + this.EnumerateMeshes ((mesh) => { + OV.SetThreeMeshPolygonOffset (mesh, false); + }); + this.EnumerateEdges ((edge) => { + edge.geometry.dispose (); + }); + this.scene.remove (this.mainEdgeObject); + this.mainEdgeObject = null; } EnumerateMeshes (enumerator) @@ -143,6 +249,18 @@ OV.ViewerGeometry = class }); } + EnumerateEdges (enumerator) + { + if (this.mainEdgeObject === null) { + return; + } + this.mainEdgeObject.traverse ((obj) => { + if (obj.isLineSegments) { + enumerator (obj); + } + }); + } + GetModelMeshUnderMouse (mouseCoords, camera, width, height) { if (this.mainObject === null) { @@ -288,20 +406,26 @@ OV.ShadingModel = class this.directionalLight.position.set (lightDir.x, lightDir.y, lightDir.z); } - CreateHighlightMaterial (highlightColor) + CreateHighlightMaterial (highlightColor, withOffset) { + let material = null; if (this.type === OV.ShadingType.Phong) { - return new THREE.MeshPhongMaterial ({ + material = new THREE.MeshPhongMaterial ({ color : highlightColor, side : THREE.DoubleSide }); } else if (this.type === OV.ShadingType.Physical) { - return new THREE.MeshStandardMaterial ({ + material = new THREE.MeshStandardMaterial ({ color : highlightColor, side : THREE.DoubleSide }); } - return null; + if (material !== null && withOffset) { + material.polygonOffset = true; + material.polygonOffsetUnit = 1; + material.polygonOffsetFactor = 1; + } + return material; } }; @@ -367,6 +491,12 @@ OV.Viewer = class this.Render (); } + SetEdgeSettings (show, color, threshold) + { + this.geometry.SetEdgeSettings (show, color, threshold); + this.Render (); + } + SetEnvironmentMap (textures) { this.shading.SetEnvironment (textures, () => { @@ -501,7 +631,7 @@ OV.Viewer = class Clear () { - this.geometry.ClearMainObject (); + this.geometry.Clear (); this.Render (); } @@ -513,6 +643,12 @@ OV.Viewer = class mesh.visible = visible; } }); + this.geometry.EnumerateEdges ((edge) => { + let visible = isVisible (edge.userData); + if (edge.visible !== visible) { + edge.visible = visible; + } + }); this.Render (); } @@ -527,7 +663,8 @@ OV.Viewer = class return highlightMaterials; } - const highlightMaterial = this.shading.CreateHighlightMaterial (highlightColor); + const showEdges = this.geometry.edgeSettings.showEdges; + const highlightMaterial = this.shading.CreateHighlightMaterial (highlightColor, showEdges); this.geometry.EnumerateMeshes ((mesh) => { let highlighted = isHighlighted (mesh.userData); if (highlighted) { diff --git a/website/o3dv/css/O3DVIcons/O3DVIcons.woff b/website/o3dv/css/O3DVIcons/O3DVIcons.woff index 22c8623..2046bcb 100644 Binary files a/website/o3dv/css/O3DVIcons/O3DVIcons.woff and b/website/o3dv/css/O3DVIcons/O3DVIcons.woff differ diff --git a/website/o3dv/css/icons.css b/website/o3dv/css/icons.css index d501338..1b32aa5 100644 --- a/website/o3dv/css/icons.css +++ b/website/o3dv/css/icons.css @@ -1,6 +1,6 @@ @font-face { font-family: "O3DVIcons"; - src: url("O3DVIcons/O3DVIcons.woff?7bda82bc77fb1b1ae8401407a3623f2c") format("woff"); + src: url("O3DVIcons/O3DVIcons.woff?9f1e775b55726eccac0336790d9d9b2f") format("woff"); } i[class^="icon-"]:before, i[class*=" icon-"]:before { @@ -38,93 +38,96 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before { .icon-donate:before { content: "\f108"; } -.icon-expand:before { +.icon-edges:before { content: "\f109"; } -.icon-export:before { +.icon-expand:before { content: "\f10a"; } -.icon-feedback:before { +.icon-export:before { content: "\f10b"; } -.icon-file_download:before { +.icon-feedback:before { content: "\f10c"; } -.icon-files:before { +.icon-file_download:before { content: "\f10d"; } -.icon-fit:before { +.icon-files:before { content: "\f10e"; } -.icon-fix_up_off:before { +.icon-fit:before { content: "\f10f"; } -.icon-fix_up_on:before { +.icon-fix_up_off:before { content: "\f110"; } -.icon-flat_list:before { +.icon-fix_up_on:before { content: "\f111"; } -.icon-flip:before { +.icon-flat_list:before { content: "\f112"; } -.icon-github:before { +.icon-flip:before { content: "\f113"; } -.icon-hidden:before { +.icon-github:before { content: "\f114"; } -.icon-info:before { +.icon-hidden:before { content: "\f115"; } -.icon-isolate:before { +.icon-info:before { content: "\f116"; } -.icon-materials:before { +.icon-isolate:before { content: "\f117"; } -.icon-meshes:before { +.icon-materials:before { content: "\f118"; } -.icon-missing_files:before { +.icon-meshes:before { content: "\f119"; } -.icon-model:before { +.icon-missing_files:before { content: "\f11a"; } -.icon-open_url:before { +.icon-model:before { content: "\f11b"; } -.icon-open:before { +.icon-open_url:before { content: "\f11c"; } -.icon-settings:before { +.icon-open:before { content: "\f11d"; } -.icon-share:before { +.icon-settings:before { content: "\f11e"; } -.icon-theme:before { +.icon-share:before { content: "\f11f"; } -.icon-tree_mesh:before { +.icon-theme:before { content: "\f120"; } -.icon-tree_view:before { +.icon-tree_mesh:before { content: "\f121"; } -.icon-twitter:before { +.icon-tree_view:before { content: "\f122"; } -.icon-up_y:before { +.icon-twitter:before { content: "\f123"; } -.icon-up_z:before { +.icon-up_y:before { content: "\f124"; } -.icon-visible:before { +.icon-up_z:before { content: "\f125"; } -.icon-warning:before { +.icon-visible:before { content: "\f126"; } +.icon-warning:before { + content: "\f127"; +} diff --git a/website/o3dv/js/exportdialog.js b/website/o3dv/js/exportdialog.js index e61c0c6..659ae4f 100644 --- a/website/o3dv/js/exportdialog.js +++ b/website/o3dv/js/exportdialog.js @@ -134,20 +134,13 @@ OV.ExportDialog = class for (let i = 0; i < exportFormat.formats.length; i++) { let format = exportFormat.formats[i]; let formatDiv = OV.AddDiv (this.formatParameters.formatSettingsDiv, 'ov_dialog_row'); - let formatLabel = OV.AddDomElement (formatDiv, 'label'); - formatLabel.setAttribute ('for', format.name); - let formatInput = OV.AddDomElement (formatLabel, 'input', 'ov_radio_button'); - formatInput.setAttribute ('type', 'radio'); - formatInput.setAttribute ('id', format.name); - formatInput.setAttribute ('name', 'format'); - OV.AddDomElement (formatLabel, 'span', null, format.name); + let formatInput = OV.AddRadioButton (formatDiv, 'format', format.name, format.name, () => { + this.formatParameters.selectedFormat = format; + }); if (i === 0) { formatInput.checked = true; this.formatParameters.selectedFormat = format; } - formatInput.addEventListener ('change', () => { - this.formatParameters.selectedFormat = format; - }); } } diff --git a/website/o3dv/js/settings.js b/website/o3dv/js/settings.js index 4589f99..c0a5c1b 100644 --- a/website/o3dv/js/settings.js +++ b/website/o3dv/js/settings.js @@ -9,6 +9,9 @@ OV.Settings = class { this.backgroundColor = new OV.Color (255, 255, 255); this.defaultColor = new OV.Color (200, 200, 200); + this.showEdges = false; + this.edgeColor = new OV.Color (0, 0, 0); + this.edgeThreshold = 10; this.themeId = OV.Theme.Light; } @@ -16,6 +19,10 @@ OV.Settings = class { this.backgroundColor = cookieHandler.GetColorVal ('ov_background_color', new OV.Color (255, 255, 255)); this.defaultColor = cookieHandler.GetColorVal ('ov_default_color', new OV.Color (200, 200, 200)); + this.showEdges = cookieHandler.GetBoolVal ('ov_show_edges', false); + this.edgeColor = cookieHandler.GetColorVal ('ov_edge_color', new OV.Color (0, 0, 0)); + this.edgeThreshold = cookieHandler.GetIntVal ('ov_edge_threshold', 10); + this.showEdges = cookieHandler.GetBoolVal ('ov_show_edges', false); this.themeId = cookieHandler.GetIntVal ('ov_theme_id', OV.Theme.Light); } @@ -23,6 +30,9 @@ OV.Settings = class { cookieHandler.SetColorVal ('ov_background_color', this.backgroundColor); cookieHandler.SetColorVal ('ov_default_color', this.defaultColor); - cookieHandler.SetStringVal ('ov_theme_id', this.themeId); + cookieHandler.SetBoolVal ('ov_show_edges', this.showEdges); + cookieHandler.SetColorVal ('ov_edge_color', this.edgeColor); + cookieHandler.SetIntVal ('ov_edge_threshold', this.edgeThreshold); + cookieHandler.SetIntVal ('ov_theme_id', this.themeId); } }; diff --git a/website/o3dv/js/sidebar.js b/website/o3dv/js/sidebar.js index 06777b6..2d866b0 100644 --- a/website/o3dv/js/sidebar.js +++ b/website/o3dv/js/sidebar.js @@ -49,6 +49,9 @@ OV.Sidebar = class onDefaultColorChange : () => { this.callbacks.onDefaultColorChange (); }, + onEdgeDisplayChange : () => { + this.callbacks.onEdgeDisplayChange (); + }, onThemeChange : () => { this.callbacks.onThemeChange (); } diff --git a/website/o3dv/js/sidebarpanels.js b/website/o3dv/js/sidebarpanels.js index c4ae1ee..ecabe48 100644 --- a/website/o3dv/js/sidebarpanels.js +++ b/website/o3dv/js/sidebarpanels.js @@ -210,6 +210,7 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel this.backgroundColorInput = null; this.defaultColorInput = null; this.defaultColorWarning = null; + this.edgeDisplayInput = null; this.themeInput = null; } @@ -252,6 +253,7 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel this.SetDefaultColor (newColor, false); } ); + this.edgeDisplayInput = this.AddEdgeDisplayParameter (this.settings.showEdges, this.settings.edgeColor, this.settings.edgeThreshold); this.themeInput = this.AddThemeParameter (this.settings.themeId); this.AddResetToDefaultsButton (); } @@ -286,12 +288,6 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel } } - SetThemeId (themeId) - { - this.settings.themeId = themeId; - this.callbacks.onThemeChange (); - } - AddColorParameter (title, description, warningText, predefinedColors, defaultValue, onChange) { let contentDiv = OV.AddDiv (this.contentDiv, 'ov_sidebar_settings_content'); @@ -344,38 +340,46 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel }; } - AddThemeParameter (defaultValue) + AddEdgeDisplayParameter (show, edgeColor, edgeThreshold) { - function AddThemeRadioButton (obj, contentDiv, themeId, themeName, onChange) + function AddRadioButton (contentDiv, id, text, onChange) { let row = OV.AddDiv (contentDiv, 'ov_sidebar_settings_row'); - let label = OV.AddDomElement (row, 'label'); - label.setAttribute ('for', themeId.toString ()); - let radio = OV.AddDomElement (label, 'input', 'ov_radio_button'); - radio.setAttribute ('type', 'radio'); - radio.setAttribute ('id', themeId.toString ()); - radio.setAttribute ('name', 'theme'); - OV.AddDomElement (label, 'span', null, themeName); - radio.addEventListener ('change', () => { - obj.SetThemeId (themeId); - if (themeId === OV.Theme.Light) { - obj.SetBackgroundColor (new OV.Color (255, 255, 255), true); - obj.SetDefaultColor (new OV.Color (200, 200, 200), true); - } else if (themeId === OV.Theme.Dark) { - obj.SetBackgroundColor (new OV.Color (42, 43, 46), true); - obj.SetDefaultColor (new OV.Color (200, 200, 200), true); - } - onChange (); - }); - return radio; + return OV.AddRadioButton (row, 'edge_display', id, text, onChange); } - function Select (radioButtons, defaultValue) - { - for (let i = 0; i < radioButtons.length; i++) { - let radioButton = radioButtons[i]; - radioButton.checked = radioButton.getAttribute ('id') === defaultValue.toString (); + let contentDiv = OV.AddDiv (this.contentDiv, 'ov_sidebar_settings_content'); + let titleDiv = OV.AddDiv (contentDiv, 'ov_sidebar_subtitle'); + OV.AddSvgIconElement (titleDiv, 'edges', 'ov_sidebar_subtitle_icon'); + OV.AddDiv (titleDiv, 'ov_sidebar_subtitle_text', 'Edge Display'); + + let buttonsDiv = OV.AddDiv (contentDiv, 'ov_sidebar_settings_padded'); + let buttons = []; + let offButton = AddRadioButton (buttonsDiv, 'off', 'Don\'t Show Edges', () => { + this.settings.showEdges = false; + this.callbacks.onEdgeDisplayChange (); + }); + let onButton = AddRadioButton (buttonsDiv, 'on', 'Show Edges', () => { + this.settings.showEdges = true; + this.callbacks.onEdgeDisplayChange (); + }); + buttons.push (offButton); + buttons.push (onButton); + + OV.SelectRadioButton (buttons, show ? 'on' : 'off'); + return { + select : (value) => { + OV.SelectRadioButton (buttons, value ? 'on' : 'off'); } + }; + } + + AddThemeParameter (defaultValue) + { + function AddRadioButton (contentDiv, themeId, themeName, onChange) + { + let row = OV.AddDiv (contentDiv, 'ov_sidebar_settings_row'); + return OV.AddRadioButton (row, 'theme', themeId.toString (), themeName, onChange); } let contentDiv = OV.AddDiv (this.contentDiv, 'ov_sidebar_settings_content'); @@ -384,16 +388,28 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel OV.AddDiv (titleDiv, 'ov_sidebar_subtitle_text', 'Appearance'); let buttonsDiv = OV.AddDiv (contentDiv, 'ov_sidebar_settings_padded'); - let result = { - buttons : [], - select: (value) => { - Select (result.buttons, value); + let buttons = []; + let lightButton = AddRadioButton (buttonsDiv, OV.Theme.Light, 'Light', () => { + this.SetBackgroundColor (new OV.Color (255, 255, 255), true); + this.SetDefaultColor (new OV.Color (200, 200, 200), true); + this.settings.themeId = OV.Theme.Light; + this.callbacks.onThemeChange (); + }); + let darkButton = AddRadioButton (buttonsDiv, OV.Theme.Dark, 'Dark', () => { + this.SetBackgroundColor (new OV.Color (42, 43, 46), true); + this.SetDefaultColor (new OV.Color (200, 200, 200), true); + this.settings.themeId = OV.Theme.Dark; + this.callbacks.onThemeChange (); + }); + buttons.push (lightButton); + buttons.push (darkButton); + + OV.SelectRadioButton (buttons, defaultValue.toString ()); + return { + select : (value) => { + OV.SelectRadioButton (buttons, value.toString ()); } }; - result.buttons.push (AddThemeRadioButton (this, buttonsDiv, OV.Theme.Light, 'Light', this.callbacks.onThemeChange)); - result.buttons.push (AddThemeRadioButton (this, buttonsDiv, OV.Theme.Dark, 'Dark', this.callbacks.onThemeChange)); - Select (result.buttons, defaultValue); - return result; } AddResetToDefaultsButton () @@ -405,11 +421,16 @@ OV.SettingsSidebarPanel = class extends OV.SidebarPanel this.settings.defaultColor = defaultSettings.defaultColor; this.backgroundColorInput.pickr.setColor ('#' + OV.ColorToHexString (defaultSettings.backgroundColor)); this.defaultColorInput.pickr.setColor ('#' + OV.ColorToHexString (defaultSettings.defaultColor)); - if (this.themeInput !== null) { - this.settings.themeId = defaultSettings.themeId; - this.themeInput.select (defaultSettings.themeId); - this.callbacks.onThemeChange (); - } + + this.settings.showEdges = defaultSettings.showEdges; + this.settings.edgeColor = defaultSettings.edgeColor; + this.settings.edgeThreshold = defaultSettings.edgeThreshold; + this.edgeDisplayInput.select (this.settings.showEdges); + this.callbacks.onEdgeDisplayChange (); + + this.settings.themeId = defaultSettings.themeId; + this.themeInput.select (this.settings.themeId); + this.callbacks.onThemeChange (); }); } }; diff --git a/website/o3dv/js/website.js b/website/o3dv/js/website.js index 59f6545..66cedfd 100644 --- a/website/o3dv/js/website.js +++ b/website/o3dv/js/website.js @@ -378,6 +378,12 @@ OV.Website = class } } + UpdateEdgeDisplay () + { + this.settings.SaveToCookies (this.cookieHandler); + this.viewer.SetEdgeSettings (this.settings.showEdges, this.settings.edgeColor, this.settings.edgeThreshold); + } + SwitchTheme (newThemeId, resetColors) { this.settings.themeId = newThemeId; @@ -398,6 +404,7 @@ OV.Website = class { let canvas = OV.AddDomElement (this.parameters.viewerDiv, 'canvas'); this.viewer.Init (canvas); + this.viewer.SetEdgeSettings (this.settings.showEdges, this.settings.edgeColor, this.settings.edgeThreshold); this.viewer.SetBackgroundColor (this.settings.backgroundColor); this.viewer.SetEnvironmentMap ([ 'assets/envmaps/grayclouds/posx.jpg', @@ -549,6 +556,9 @@ OV.Website = class } this.viewer.Render (); }, + onEdgeDisplayChange : () => { + this.UpdateEdgeDisplay (); + }, onThemeChange : () => { this.SwitchTheme (this.settings.themeId, true); },