├── .gitignore
├── .babelrc
├── .prettierrc
├── src
├── lib
│ ├── isIFrame.js
│ ├── parseQueryString.js
│ └── Colors.js
├── index.js
├── views
│ ├── VisibleOption.js
│ ├── SelectOption.js
│ ├── TextureOption.js
│ └── ColorOption.js
├── OptionsView.js
├── Options.js
├── Configurator.js
├── schema.json
└── Viewer.js
├── app.js
├── webpack.config.js
├── package.json
├── index.html
├── samples
├── car.json
├── chair.json
├── robot.json
└── orb.json
├── styles.css
├── README.md
└── dist
└── SketchfabConfigurator-1.0.2.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 4,
4 | "bracketSpacing": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/isIFrame.js:
--------------------------------------------------------------------------------
1 | export default function isIFrame() {
2 | try {
3 | return window.self !== window.top;
4 | } catch (e) {
5 | return true;
6 | }
7 | }
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var iframeEl = document.getElementById('api-frame');
2 | var optionsEl = document.querySelector('.options');
3 | var configurator = new SketchfabConfigurator.Configurator(iframeEl, optionsEl);
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import Configurator from './Configurator';
4 | import Viewer from './Viewer';
5 | import Options from './Options';
6 | import OptionsView from './OptionsView';
7 |
8 | export { Configurator, Viewer, Options, OptionsView };
9 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const package = require('./package.json');
2 | const path = require('path');
3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
4 |
5 | module.exports = {
6 | entry: ['./src/index.js'],
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | filename: `SketchfabConfigurator-${package.version}.js`,
10 | library: 'SketchfabConfigurator'
11 | },
12 | module: {
13 | rules: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }]
14 | },
15 | // plugins: [new BundleAnalyzerPlugin()]
16 | };
17 |
--------------------------------------------------------------------------------
/src/lib/parseQueryString.js:
--------------------------------------------------------------------------------
1 | /**
2 | * https://gist.github.com/Manc/9409355
3 | * Convert a URL or just the query part of a URL to an
4 | * object with all keys and values.
5 | * Usage examples:
6 | * // Get "GET" parameters from current request as object:
7 | * var parameters = parseQueryString(window.location.search);
8 | */
9 | export default function parseQueryString(query) {
10 | var obj = {},
11 | qPos = query.indexOf('?'),
12 | tokens = query.substr(qPos + 1).split('&'),
13 | i = tokens.length - 1;
14 | if (qPos !== -1 || query.indexOf('=') !== -1) {
15 | for (; i >= 0; i--) {
16 | var s = tokens[i].split('=');
17 | obj[unescape(s[0])] = s.hasOwnProperty(1) ? unescape(s[1]) : null;
18 | }
19 | }
20 | return obj;
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "configurator",
3 | "version": "1.0.6",
4 | "description": "Configurator demo",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "watch": "webpack --watch --mode=development",
8 | "build": "webpack --mode=production",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "version": "npm run build && git add -A dist",
11 | "docs": "npx jsdoc src/*.js src/**/*.js -d docs"
12 | },
13 | "author": "Sketchfab",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "babel-core": "^6.26.0",
17 | "babel-loader": "^7.1.4",
18 | "babel-preset-env": "^1.6.1",
19 | "webpack": "^4.4.1",
20 | "webpack-bundle-analyzer": "^2.11.1",
21 | "webpack-cli": "^2.0.13"
22 | },
23 | "dependencies": {
24 | "ajv": "^6.4.0",
25 | "babel-polyfill": "^6.26.0",
26 | "mustache": "^2.3.0",
27 | "unfetch": "^3.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sketchfab Configurator
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/samples/car.json:
--------------------------------------------------------------------------------
1 | {
2 | "model": "36e9d3598c554bb69f3d9cd00e161818",
3 | "config": [
4 | {
5 | "name": "Paint color",
6 | "type": "color",
7 | "material": ["body", "body_0"],
8 | "default": "#790300"
9 | },
10 | {
11 | "name": "Seat color",
12 | "type": "color",
13 | "material": ["leather_1", "leather_2"],
14 | "options": [
15 | {
16 | "color": "#333333",
17 | "name": "Black"
18 | },
19 | {
20 | "color": "#FFFFFF",
21 | "name": "White"
22 | },
23 | {
24 | "color": "#803A00",
25 | "name": "Brown"
26 | }
27 | ],
28 | "default": "#803A00"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/samples/chair.json:
--------------------------------------------------------------------------------
1 | {
2 | "model": "4d9d1eaa3efe44099a223192d418c17d",
3 | "params": {
4 | "camera": 0,
5 | "preload": 1,
6 | "ui_controls": 0,
7 | "ui_infos": 0,
8 | "ui_hint": 2,
9 | "ui_stop": 0,
10 | "internal": 1,
11 | "watermark": 0,
12 | "double_click": 0,
13 | "autospin": -0.1
14 | },
15 | "config": [
16 | {
17 | "name": "Select color",
18 | "type": "color",
19 | "material": ["seat"],
20 | "options": [
21 | {
22 | "color": "#6FC0E7",
23 | "name": "Light Blue"
24 | },
25 | {
26 | "color": "#2B4462",
27 | "name": "Dark Blue"
28 | },
29 | {
30 | "color": "#80B744",
31 | "name": "Green"
32 | },
33 | {
34 | "color": "#cc1f16",
35 | "name": "Red"
36 | }
37 | ],
38 | "default": "#2B4462"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/lib/Colors.js:
--------------------------------------------------------------------------------
1 | const GAMMA = 2.4;
2 |
3 | function srgbToLinear1(c) {
4 | var v = 0.0;
5 | if (c < 0.04045) {
6 | if (c >= 0.0) v = c * (1.0 / 12.92);
7 | } else {
8 | v = Math.pow((c + 0.055) * (1.0 / 1.055), GAMMA);
9 | }
10 | return v;
11 | }
12 |
13 | function srgbToLinear(c, out) {
14 | var col = out || new Array(c.length);
15 |
16 | if (c.length > 2 && c.length < 5) {
17 | col[0] = srgbToLinear1(c[0]);
18 | col[1] = srgbToLinear1(c[1]);
19 | col[2] = srgbToLinear1(c[2]);
20 | if (col.length > 3 && c.length > 3) col[3] = c[3];
21 | } else {
22 | throw new Error('Invalid color. Expected 3 or 4 components, but got ' + c.length);
23 | }
24 | return col;
25 | }
26 |
27 | function hexToRgb(hexColor) {
28 | var m = hexColor.match(/^#([0-9a-f]{6})$/i);
29 | if (m) {
30 | return [
31 | parseInt(m[1].substr(0, 2), 16) / 255,
32 | parseInt(m[1].substr(2, 2), 16) / 255,
33 | parseInt(m[1].substr(4, 2), 16) / 255
34 | ];
35 | } else {
36 | throw new Error('Invalid color: ' + hexColor);
37 | }
38 | }
39 |
40 | export {
41 | srgbToLinear,
42 | hexToRgb
43 | };
44 |
--------------------------------------------------------------------------------
/samples/robot.json:
--------------------------------------------------------------------------------
1 | {
2 | "model": "58425893f23d4ee691014e43fa12980c",
3 | "params": {
4 | "camera": 0,
5 | "preload": 1,
6 | "animation_autoplay": 0
7 | },
8 | "config": [
9 | {
10 | "name": "Arms",
11 | "type": "visible",
12 | "selector": "[name=\"g_arm\"]",
13 | "default": false
14 | },
15 |
16 | {
17 | "name": "Plastic color",
18 | "type": "color",
19 | "material": "white_plastic",
20 | "default": "#1CAAD9"
21 | },
22 |
23 | {
24 | "name": "Antenna side",
25 | "type": "select",
26 | "options": [
27 | {
28 | "selector": "",
29 | "name": "No antenna"
30 | },
31 | {
32 | "selector": "[name=\"g_head_LEFT\"] [name=\"g_aerial\"]",
33 | "name": "Left"
34 | },
35 | {
36 | "selector": "[name=\"g_head_RIGHT\"] [name=\"g_aerial\"]",
37 | "name": "Right"
38 | }
39 | ],
40 | "default": 0
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/views/VisibleOption.js:
--------------------------------------------------------------------------------
1 | import Mustache from 'mustache';
2 |
3 | /**
4 | * View for 'visible' option
5 | */
6 | class VisibleOption {
7 | /**
8 | * @param {object} model Options instance
9 | * @param {number} index Index of option
10 | */
11 | constructor(model, index) {
12 | this.model = model;
13 | this.index = index;
14 | }
15 |
16 | _generateId() {
17 | return 'control_' + Math.floor(Math.random() * 10000);
18 | }
19 |
20 | /**
21 | * Renders the view
22 | */
23 | render() {
24 | if (!this.el) {
25 | this.el = document.createElement('DIV');
26 | this.el.className = 'option option--visible';
27 | var value = this.model.getOptionValue(this.index);
28 | var html = Mustache.render(this.template, {
29 | id: this._generateId(),
30 | index: this.index,
31 | option: this.model.options[this.index],
32 | value: value
33 | });
34 | this.el.innerHTML = html;
35 | }
36 | return this;
37 | }
38 | }
39 |
40 | VisibleOption.prototype.template = `
41 |
42 |
43 |
44 |
45 | `;
46 |
47 | export default VisibleOption;
48 |
--------------------------------------------------------------------------------
/src/views/SelectOption.js:
--------------------------------------------------------------------------------
1 | import Mustache from 'mustache';
2 |
3 | /**
4 | * View for 'select' option
5 | */
6 | class SelectOption {
7 | /**
8 | * @param {object} model Options instance
9 | * @param {number} index Index of option
10 | */
11 | constructor(model, index) {
12 | this.model = model;
13 | this.index = index;
14 | }
15 |
16 | _generateId() {
17 | return 'control_' + Math.floor(Math.random() * 10000);
18 | }
19 |
20 | /**
21 | * Renders the view
22 | */
23 | render() {
24 | if (!this.el) {
25 | this.el = document.createElement('DIV');
26 | this.el.className = 'option option--select';
27 |
28 | var currentValue = this.model.getOptionValue(this.index);
29 | var optionsWithIndex = this.model.options[this.index].options.map((opt, index) => {
30 | return Object.assign({}, opt, {
31 | index: index,
32 | isSelected: index === currentValue
33 | });
34 | });
35 |
36 | var html = Mustache.render(this.template, {
37 | id: this._generateId(),
38 | index: this.index,
39 | option: this.model.options[this.index],
40 | options: optionsWithIndex,
41 | value: currentValue
42 | });
43 | this.el.innerHTML = html;
44 | }
45 | return this;
46 | }
47 | }
48 |
49 | SelectOption.prototype.template = `
50 |
51 |
52 |
57 |
58 | `;
59 |
60 | export default SelectOption;
61 |
--------------------------------------------------------------------------------
/src/views/TextureOption.js:
--------------------------------------------------------------------------------
1 | import Mustache from 'mustache';
2 |
3 | /**
4 | * View for 'texture' option
5 | */
6 | class TextureOption {
7 | /**
8 | * @param {object} model Options instance
9 | * @param {number} index Index of option
10 | */
11 | constructor(model, index) {
12 | this.model = model;
13 | this.index = index;
14 | }
15 |
16 | _generateId() {
17 | return 'control_' + Math.floor(Math.random() * 10000);
18 | }
19 |
20 | /**
21 | * Renders the view
22 | */
23 | render() {
24 | if (!this.el) {
25 | this.el = document.createElement('DIV');
26 | this.el.className = 'option option--texture';
27 |
28 | var currentValue = this.model.getOptionValue(this.index);
29 | var optionsWithIndex = this.model.options[this.index].options.map((opt, index) => {
30 | return Object.assign({}, opt, {
31 | currentIndex: index,
32 | isSelected: index === currentValue
33 | });
34 | });
35 |
36 | var html = Mustache.render(this.template, {
37 | id: this._generateId(),
38 | index: this.index,
39 | option: this.model.options[this.index],
40 | options: optionsWithIndex,
41 | value: currentValue
42 | });
43 | this.el.innerHTML = html;
44 | }
45 | return this;
46 | }
47 | }
48 |
49 | TextureOption.prototype.template = `
50 |
51 |
62 | `;
63 |
64 | export default TextureOption;
65 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | @import 'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css';
2 |
3 | html,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | width: 100%;
8 | height: 100%;
9 | font-family: sans-serif;
10 | }
11 |
12 | iframe {
13 | display: block;
14 | border: 0;
15 | width: 100%;
16 | height: 100%;
17 | }
18 |
19 | .app {
20 | display: flex;
21 | width: 100%;
22 | height: 100%;
23 | }
24 |
25 | #api-frame {
26 | flex: 1 1 auto;
27 | }
28 |
29 | .options {
30 | box-sizing: border-box;
31 | flex: 0 0 300px;
32 | padding: 30px;
33 | overflow: auto;
34 | }
35 |
36 | .option {
37 | display: flex;
38 | padding: 16px 0;
39 | border-bottom: 1px solid #ccc;
40 | }
41 |
42 | .option:first-child {
43 | border-top: 1px solid #ccc;
44 | }
45 |
46 | .option label {
47 | flex: 0 0 130px;
48 | }
49 |
50 | .color {
51 | display: flex;
52 | align-items: center;
53 | padding: 4px 0;
54 | }
55 |
56 | .color input[type='radio'] {
57 | position: absolute;
58 | opacity: 0;
59 | pointer-events: none;
60 | }
61 |
62 | .color__swatch {
63 | display: inline-block;
64 | width: 24px;
65 | height: 24px;
66 | border-radius: 50%;
67 | border: 1px solid rgba(0, 0, 0, 0.5);
68 | margin-right: 4px;
69 | }
70 |
71 | .color input:checked + .color__swatch {
72 | box-shadow: 0 0 0 2px #000;
73 | }
74 |
75 | .color:hover .color__swatch {
76 | box-shadow: 0 0 0 2px #999;
77 | }
78 |
79 | .texture {
80 | display: block;
81 | position: relative;
82 | margin-bottom: 10px;
83 | }
84 |
85 | .texture input[type='radio'] {
86 | position: absolute;
87 | opacity: 0;
88 | pointer-events: none;
89 | }
90 |
91 | .texture__preview {
92 | display: block;
93 | }
94 |
95 | .texture__preview img {
96 | display: block;
97 | max-width: 100%;
98 | height: auto;
99 | }
100 |
101 | .texture input:checked + .texture__preview > img {
102 | box-shadow: 0 0 0 2px #000;
103 | }
104 |
105 | .texture__name {
106 | display: block;
107 | margin-top: 4px;
108 | }
109 |
--------------------------------------------------------------------------------
/src/views/ColorOption.js:
--------------------------------------------------------------------------------
1 | import Mustache from 'mustache';
2 |
3 | /**
4 | * View for 'color' option
5 | */
6 | class ColorOption {
7 | /**
8 | * @param {object} model Options instance
9 | * @param {number} index Index of option
10 | */
11 | constructor(model, index) {
12 | this.model = model;
13 | this.index = index;
14 | }
15 |
16 | _generateId() {
17 | return 'control_' + Math.floor(Math.random() * 10000);
18 | }
19 |
20 | /**
21 | * Renders the view
22 | */
23 | render() {
24 | if (!this.el) {
25 | this.el = document.createElement('DIV');
26 | this.el.className = 'option option--color';
27 |
28 | const currentValue = this.model.getOptionValue(this.index);
29 | let optionsForTemplate;
30 | if (this.model.options[this.index].options !== undefined) {
31 | optionsForTemplate = this.model.options[this.index].options.map((opt, index) => {
32 | return Object.assign({}, opt, {
33 | isSelected: opt.color.toUpperCase() === currentValue.toUpperCase()
34 | });
35 | });
36 | } else {
37 | optionsForTemplate = null;
38 | }
39 |
40 | const html = Mustache.render(this.template, {
41 | id: this._generateId(),
42 | index: this.index,
43 | option: this.model.options[this.index],
44 | options: optionsForTemplate,
45 | value: this.model.getOptionValue(this.index)
46 | });
47 | this.el.innerHTML = html;
48 | }
49 | return this;
50 | }
51 | }
52 |
53 | ColorOption.prototype.template = `
54 |
55 |
56 | {{#options}}
57 |
62 | {{/options}}
63 | {{^options}}
64 |
65 | {{/options}}
66 |
67 | `;
68 |
69 | export default ColorOption;
70 |
--------------------------------------------------------------------------------
/src/OptionsView.js:
--------------------------------------------------------------------------------
1 | import ColorOption from './views/ColorOption';
2 | import SelectOption from './views/SelectOption';
3 | import TextureOption from './views/TextureOption';
4 | import VisibleOption from './views/VisibleOption';
5 |
6 | const FORM_CLASS_NAME = 'sketchfab-configurator';
7 |
8 | /**
9 | * View for the configurator UI.
10 | *
11 | * The OptionsView will create subviews for each option and listen for all `change` events.
12 | */
13 | class OptionsView {
14 | /**
15 | *
16 | * @param {object} el Placeholder DOM element for the UI
17 | * @param {object} model Options instance
18 | */
19 | constructor(el, model) {
20 | this.el = el;
21 | this.model = model;
22 | this.subviews = [];
23 | this.isRendered = false;
24 | this._handleOptionChange = this._handleOptionChange.bind(this);
25 | this.el.addEventListener('change', this._handleOptionChange, false);
26 | this.render();
27 | }
28 |
29 | /**
30 | * Renders the view
31 | */
32 | render() {
33 | if (this.isRendered) {
34 | return;
35 | }
36 |
37 | this.formEl = document.createElement('form');
38 | this.formEl.className = FORM_CLASS_NAME;
39 | this.el.appendChild(this.formEl);
40 |
41 | const classes = {
42 | color: ColorOption,
43 | visible: VisibleOption,
44 | select: SelectOption,
45 | texture: TextureOption
46 | };
47 |
48 | var subview;
49 | for (var i = 0, l = this.model.options.length; i < l; i++) {
50 | subview = new classes[this.model.options[i].type](this.model, i);
51 | subview.render();
52 | this.subviews.push(subview);
53 | this.formEl.appendChild(subview.el);
54 | }
55 |
56 | this.isRendered = true;
57 | }
58 |
59 | /**
60 | * Disposes the view
61 | * Removes event listeners and empties the DOM element.
62 | */
63 | dispose() {
64 | this.formEl.removeEventListener('change', this._handleOptionChange, false);
65 | this.el.innerHTML = '';
66 | this.subviews = [];
67 | this.isRendered = false;
68 | }
69 |
70 | _handleOptionChange(e) {
71 | e.preventDefault();
72 | const target = e.target;
73 | const optionIndex = parseInt(target.getAttribute('data-option'), 10);
74 | const value = target.type === 'checkbox' ? !!target.checked : target.value;
75 | this.model.setOptionValue(optionIndex, value);
76 | }
77 | }
78 |
79 | export default OptionsView;
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Configurator Framework
2 |
3 | The Configurator Framework is a javascript library for building 3D configurators with Sketchfab Viewer API.
4 | Configurators are described by a JSON file that are consummed by the framework.
5 |
6 | ## Supported option types
7 |
8 | ### Color
9 |
10 | The `color` option allows the user to change the color of a material.
11 | The selected color is applied to Diffuse, Diffuse (PBR) and Albedo (PBR) channels.
12 |
13 | ```json
14 | {
15 | "name": "Plastic color",
16 | "type": "color",
17 | "material": "white_plastic",
18 | "default": "#1CAAD9"
19 | },
20 | ```
21 |
22 | You can also include a list of predefined colors:
23 |
24 | ```json
25 | {
26 | "name": "Seat color",
27 | "type": "color",
28 | "material": ["leather_1", "leather_2"],
29 | "options": [
30 | {
31 | "color": "#333333",
32 | "name": "Black"
33 | },
34 | {
35 | "color": "#FFFFFF",
36 | "name": "White"
37 | },
38 | {
39 | "color": "#803A00",
40 | "name": "Brown"
41 | }
42 | ],
43 | "default": "#803A00"
44 | }
45 | ```
46 |
47 | ### Visible
48 |
49 | The `visible` option allows the user to show and hide an object.
50 |
51 | ```json
52 | {
53 | "name": "Arms",
54 | "type": "visible",
55 | "selector": "[name=\"g_arm\"]",
56 | "default": false
57 | },
58 | ```
59 |
60 | ### Select
61 |
62 | The `select` option allows the user to select an object among a list of objects.
63 | Only the selected object will be visible.
64 |
65 | ```javascript
66 | {
67 | "name": "Antenna side",
68 | "type": "select",
69 | "options": [
70 | {
71 | "selector": "",
72 | "name": "No antenna"
73 | },
74 | {
75 | "selector": "[name=\"g_head_LEFT\"] [name=\"g_aerial\"]",
76 | "name": "Left"
77 | },
78 | {
79 | "selector": "[name=\"g_head_RIGHT\"] [name=\"g_aerial\"]",
80 | "name": "Right"
81 | }
82 | ],
83 | "default": 0
84 | }
85 | ```
86 |
87 | ### Texture
88 |
89 | The `texture` option allows the user to change the texture of selected channels of a material.
90 | Images must be CORS-enabled.
91 |
92 | ```javascript
93 | {
94 | "name": "Face",
95 | "type": "texture",
96 | "material": "face",
97 | "channels": ["AlbedoPBR", "EmitColor"],
98 | "options": [
99 | {
100 | "name": "Default",
101 | "url": "https://example.com/face-default.png"
102 | },
103 | {
104 | "name": "Happy",
105 | "url": "https://example.com/face-happy.jpg"
106 | },
107 | {
108 | "name": "Sleepy",
109 | "url": "https://example.com/face-sleepy.jpg"
110 | }
111 | ],
112 | "default": 0
113 | }
114 | ```
115 |
116 | ## API Documentation
117 |
118 | * `npm run docs` to generate the docs
119 | * Doc will be generated in `docs/`
120 |
121 | ## Development
122 |
123 | * `npm install` to install dependencies
124 | * `npm run watch` to build/watch for dev
125 |
126 | ## Building for production / Release
127 |
128 | * `git checkout master`
129 | * `npm install` to install dependencies.
130 | * `npm version x.x.x` where `x.x.x` is [a valid version](https://docs.npmjs.com/cli/version).
131 | * this will update the version in `package.json`
132 | * build the library for production (`npm run build`)
133 | * create a new commit
134 | * tag the commit with the new version
135 | * Push the tag to github
136 |
137 | ## Todo
138 |
139 | * [ ] Use material values as default values
140 | * [ ] Support textures or full materials
141 | * [ ] Support scale/translation/rotation
142 | * [ ] Custom color picker
143 | * [ ] UI Customization
144 | * [ ] Add presets for changing multiple properties at once
145 | * [ ] Interactivity? (trigger animation, move camera)
146 |
--------------------------------------------------------------------------------
/src/Options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Model for options
3 | */
4 | class Options {
5 | /**
6 | *
7 | * @param {object[]} options List of options
8 | * @param {object} viewer Viewer instance
9 | */
10 | constructor(options, viewer) {
11 | this.options = options;
12 | this.viewer = viewer;
13 | this.values = [];
14 | this.initialize();
15 | }
16 |
17 | initialize() {
18 | for (var i = 0, l = this.options.length; i < l; i++) {
19 | switch (this.options[i].type) {
20 | case 'visible':
21 | this.setOptionValue(i, !!this.options[i].default);
22 | break;
23 | case 'color':
24 | this.setOptionValue(i, this.options[i].default);
25 | break;
26 | case 'select':
27 | if (this.options[i].default) {
28 | this.setOptionValue(i, this.options[i].default);
29 | } else {
30 | this.setOptionValue(i, 0);
31 | }
32 | break;
33 | case 'texture':
34 | this.setOptionValue(i, this.options[i].default);
35 | break;
36 | }
37 | }
38 | }
39 |
40 | /**
41 | * Set an option value
42 | * @param {number} optionIndex Index of option
43 | * @param {*} value Value
44 | */
45 | setOptionValue(optionIndex, value) {
46 | const option = this.options[optionIndex];
47 | const fn = {
48 | color: 'applyOptionColor',
49 | visible: 'applyOptionVisible',
50 | select: 'applyOptionSelect',
51 | texture: 'applyOptionTexture'
52 | };
53 |
54 | if (option.type === 'select' || option.type === 'texture') {
55 | value = parseInt(value, 10);
56 | }
57 |
58 | if (option.type === 'visible') {
59 | value = Boolean(value);
60 | }
61 |
62 | this.values[optionIndex] = value;
63 | this[fn[option.type]].apply(this, [optionIndex, value]);
64 | }
65 |
66 | /**
67 | * Gets the value of an option
68 | * @param {number} optionIndex Index of option
69 | */
70 | getOptionValue(optionIndex) {
71 | return this.values[optionIndex];
72 | }
73 |
74 | applyOptionColor(optionIndex, color) {
75 | const option = this.options[optionIndex];
76 |
77 | if (option.type !== 'color') {
78 | throw new Error('Option is not of "color" type');
79 | }
80 |
81 | const material = option.material;
82 | this.viewer.setColor(material, color);
83 | }
84 |
85 | applyOptionVisible(optionIndex, isVisible) {
86 | const option = this.options[optionIndex];
87 |
88 | if (option.type !== 'visible') {
89 | throw new Error('Option is not of "visible" type');
90 | }
91 |
92 | const selector = option.selector;
93 | if (isVisible) {
94 | this.viewer.show(selector);
95 | } else {
96 | this.viewer.hide(selector);
97 | }
98 | }
99 |
100 | applyOptionSelect(optionIndex, selectedIndex) {
101 | const option = this.options[optionIndex];
102 |
103 | if (option.type !== 'select') {
104 | throw new Error('Option is not of "select" type');
105 | }
106 |
107 | for (var i = 0, l = option.options.length; i < l; i++) {
108 | if (option.options[i].selector === '') {
109 | continue;
110 | }
111 |
112 | if (i === selectedIndex) {
113 | this.viewer.show(option.options[i].selector);
114 | } else {
115 | this.viewer.hide(option.options[i].selector);
116 | }
117 | }
118 | }
119 |
120 | applyOptionTexture(optionIndex, selectedIndex) {
121 | const option = this.options[optionIndex];
122 |
123 | if (option.type !== 'texture') {
124 | throw new Error('Option is not of "texture" type');
125 | }
126 |
127 | for (var i = 0, l = option.options.length; i < l; i++) {
128 | if (i === selectedIndex) {
129 | this.viewer.setTexture(option.material, option.channels, option.options[i].url);
130 | }
131 | }
132 | }
133 | }
134 |
135 | export default Options;
136 |
--------------------------------------------------------------------------------
/src/Configurator.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import Ajv from 'ajv';
3 | import fetch from 'unfetch';
4 | import isIFrame from './lib/isIFrame';
5 | import parseQueryString from './lib/parseQueryString';
6 | import Viewer from './Viewer';
7 | import Options from './Options';
8 | import OptionsView from './OptionsView';
9 | const schema = require('./schema.json');
10 |
11 | const ALLOW_EMBED = true;
12 |
13 | /**
14 | * Main Configurator class
15 | */
16 | class Configurator {
17 | /**
18 | * @param {object} iframeEl iframe DOM element for the viewer
19 | * @param {object} optionsEl Placeholder DOM element for the UI
20 | * @param {object} config Configuration object
21 | */
22 | constructor(iframeEl, optionsEl, config = null) {
23 | this.iframeEl = iframeEl;
24 | this.optionsEl = optionsEl;
25 | this.optionView = null;
26 | this.viewer = null;
27 | this.config = config;
28 |
29 | if (!ALLOW_EMBED && isIFrame()) {
30 | this._renderFatalError('This page is for preview only and cannot be embedded.');
31 | return;
32 | }
33 |
34 | let promiseConfig;
35 |
36 | if (this.config) {
37 | promiseConfig = Promise.resolve(this.config);
38 | } else {
39 | var parameters = parseQueryString(window.location.search);
40 | if (parameters.hasOwnProperty('config')) {
41 | console.log('Loading config from URL', parameters.config);
42 | promiseConfig = this._loadConfig(parameters.config);
43 | } else if (window.defaultConfigUrl) {
44 | console.log('Loading default config URL', window.defaultConfigUrl);
45 | promiseConfig = this._loadConfig(window.defaultConfigUrl);
46 | } else if (window.defaultConfig) {
47 | console.log('Loading config', window.defaultConfig);
48 | promiseConfig = Promise.resolve(window.defaultConfig);
49 | } else {
50 | promiseConfig = Promise.reject('No configuration found');
51 | }
52 | }
53 |
54 | promiseConfig
55 | .then(config => {
56 | let validation = this._validate(config);
57 | if (validation.valid === false) {
58 | console.warn(validation.errors);
59 | }
60 | this.config = config;
61 | this._initialize();
62 | })
63 | .catch(error => {
64 | console.error(error);
65 | });
66 | }
67 |
68 | _initialize() {
69 | const config = this.config;
70 | this.viewer = new Viewer(
71 | iframeEl,
72 | config.model,
73 | () => {
74 | this.optionView = new OptionsView(
75 | optionsEl,
76 | new Options(config.config, this.viewer)
77 | );
78 | },
79 | {
80 | params: config.params ? config.params : {}
81 | }
82 | );
83 | }
84 |
85 | _renderFatalError(message) {
86 | const styles = `
87 | position: absolute;
88 | top: 0;
89 | left: 0;
90 | width: 96%;
91 | padding: 2%;
92 | `;
93 | const out = `${message}
`;
94 | const div = document.createElement('DIV');
95 | div.innerHTML = out;
96 | document.body.appendChild(div);
97 | }
98 |
99 | _validate(config) {
100 | const ajv = new Ajv();
101 | const validate = ajv.compile(schema);
102 | const valid = validate(config);
103 | if (valid) {
104 | return {
105 | valid: true
106 | };
107 | } else {
108 | return {
109 | valid: false,
110 | errors: validate.errors
111 | };
112 | }
113 | }
114 |
115 | /**
116 | * Disposes the configurator
117 | */
118 | dispose() {
119 | this.optionView.dispose();
120 | this.viewer.dispose();
121 | }
122 |
123 | _loadConfig(url) {
124 | return fetch(url, {
125 | method: 'GET',
126 | mode: 'cors'
127 | }).then(function(response) {
128 | return response.json();
129 | });
130 | }
131 | }
132 |
133 | export default Configurator;
134 |
--------------------------------------------------------------------------------
/samples/orb.json:
--------------------------------------------------------------------------------
1 | {
2 | "model": "d08045aeacdf487096a4b2cd1ed8845f",
3 | "params": {
4 | "camera": 0,
5 | "preload": 1,
6 | "ui_infos": 0,
7 | "ui_controls": 0,
8 | "watermark": 0,
9 | "ui_stop": 0,
10 | "autostart": 1
11 | },
12 | "config": [
13 | {
14 | "name": "Texture",
15 | "type": "texture",
16 | "material": "main_material",
17 | "channels": ["AlbedoPBR"],
18 | "options": [
19 | {
20 | "name": "Yellow",
21 | "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI4cHgiIGhlaWdodD0iMTI4cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1MS4yICg1NzUxOSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+eWVsbG93PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9InllbGxvdyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgZmlsbD0iI0ZGRkYwMCIgeD0iMCIgeT0iMCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiPjwvcmVjdD4KICAgICAgICA8dGV4dCBpZD0iWUVMTE9XIiBmb250LWZhbWlseT0iT3BlblNhbnMtRXh0cmFib2xkLCBPcGVuIFNhbnMiIGZvbnQtc2l6ZT0iMTgiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiMwMDAwMDAiPgogICAgICAgICAgICA8dHNwYW4geD0iMjYuNTgzOTg0NCIgeT0iNzQiPllFTExPVzwvdHNwYW4+CiAgICAgICAgPC90ZXh0PgogICAgPC9nPgo8L3N2Zz4="
22 | },
23 | {
24 | "name": "Magenta",
25 | "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI4cHgiIGhlaWdodD0iMTI4cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1MS4yICg1NzUxOSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+bWFnZW50YTwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJtYWdlbnRhIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjRkYwMEZGIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PC9yZWN0PgogICAgICAgIDx0ZXh0IGlkPSJNQUdFTlRBIiBmb250LWZhbWlseT0iT3BlblNhbnMtRXh0cmFib2xkLCBPcGVuIFNhbnMiIGZvbnQtc2l6ZT0iMTgiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiMwMDAwMDAiPgogICAgICAgICAgICA8dHNwYW4geD0iMTguMzA0Njg3NSIgeT0iNzQiPk1BR0VOVEE8L3RzcGFuPgogICAgICAgIDwvdGV4dD4KICAgIDwvZz4KPC9zdmc+"
26 | },
27 | {
28 | "name": "Cyan",
29 | "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI4cHgiIGhlaWdodD0iMTI4cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1MS4yICg1NzUxOSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+Y3lhbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJjeWFuIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjMDBGRkZGIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PC9yZWN0PgogICAgICAgIDx0ZXh0IGlkPSJDWUFOIiBmb250LWZhbWlseT0iT3BlblNhbnMtRXh0cmFib2xkLCBPcGVuIFNhbnMiIGZvbnQtc2l6ZT0iMTgiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiMwMDAwMDAiPgogICAgICAgICAgICA8dHNwYW4geD0iMzguNjQyNTc4MSIgeT0iNzQiPkNZQU48L3RzcGFuPgogICAgICAgIDwvdGV4dD4KICAgIDwvZz4KPC9zdmc+"
30 | },
31 | {
32 | "name": "Test",
33 | "url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI4cHgiIGhlaWdodD0iMTI4cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1MS4yICg1NzUxOSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+dGVzdDwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJ0ZXN0IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLTIiIGZpbGw9IiNGRjAwMDAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxMjgiPjwvcmVjdD4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLTItQ29weSIgZmlsbD0iI0ZGRkYwMCIgeD0iMTYiIHk9IjAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxMjgiPjwvcmVjdD4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLTItQ29weS0yIiBmaWxsPSIjMDBGRjAwIiB4PSIzMiIgeT0iMCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEyOCI+PC9yZWN0PgogICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtMi1Db3B5LTMiIGZpbGw9IiMwMEZGRkYiIHg9IjQ4IiB5PSIwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTI4Ij48L3JlY3Q+CiAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS0yLUNvcHktNCIgZmlsbD0iIzAwMDBGRiIgeD0iNjQiIHk9IjAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxMjgiPjwvcmVjdD4KICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLTItQ29weS01IiBmaWxsPSIjRkYwMEZGIiB4PSI4MCIgeT0iMCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEyOCI+PC9yZWN0PgogICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtMi1Db3B5LTYiIGZpbGw9IiNGRkZGRkYiIHg9Ijk2IiB5PSIwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTI4Ij48L3JlY3Q+CiAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS0yLUNvcHktNyIgZmlsbD0iIzAwMDAwMCIgeD0iMTEyIiB5PSIwIiB3aWR0aD0iMTYiIGhlaWdodD0iMTI4Ij48L3JlY3Q+CiAgICA8L2c+Cjwvc3ZnPg=="
34 | }
35 | ],
36 | "default": 0
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/src/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "Configurator",
4 | "description": "A configurator configuration",
5 | "type": "object",
6 | "properties": {
7 | "model": {
8 | "description": "URLID of a Sketchfab model",
9 | "type": "string"
10 | },
11 | "params": {
12 | "description": "Embed parameters",
13 | "type": "object"
14 | },
15 | "config": {
16 | "description": "Description of what can be configured",
17 | "type": "array",
18 | "items": {
19 | "anyOf": [
20 | { "$ref": "#/definitions/visibleOption" },
21 | { "$ref": "#/definitions/colorOption" },
22 | { "$ref": "#/definitions/selectOption" },
23 | { "$ref": "#/definitions/textureOption" }
24 | ]
25 | }
26 | },
27 | "extra": {
28 | "description": "Extra data for extending the format",
29 | "type": "object"
30 | }
31 | },
32 | "required": ["model", "config"],
33 | "definitions": {
34 | "namedColor": {
35 | "$id": "#namedColor",
36 | "type": "object",
37 | "properties": {
38 | "name": {
39 | "description": "Human readable name",
40 | "type": "string"
41 | },
42 | "color": {
43 | "description": "Hexadecimal RGB color",
44 | "type": "string"
45 | }
46 | },
47 | "required": ["name", "color"]
48 | },
49 |
50 | "predefinedTexture": {
51 | "$id": "#predefinedTexture",
52 | "type": "object",
53 | "properties": {
54 | "name": {
55 | "description": "Human readable name",
56 | "type": "string"
57 | },
58 | "url": {
59 | "description": "Image URL",
60 | "type": "string"
61 | }
62 | },
63 | "required": ["name", "url"]
64 | },
65 |
66 | "modelPart": {
67 | "$id": "#modelPart",
68 | "type": "object",
69 | "properties": {
70 | "name": {
71 | "description": "Human readable name",
72 | "type": "string"
73 | },
74 | "selector": {
75 | "description": "CSS selector",
76 | "$ref": "#/definitions/selector"
77 | }
78 | },
79 | "required": ["name", "selector"]
80 | },
81 |
82 | "selector": {
83 | "$id": "#selector",
84 | "type": ["string", "array"],
85 | "items": {
86 | "type": "string"
87 | }
88 | },
89 |
90 | "visibleOption": {
91 | "$id": "#visibleOption",
92 | "type": "object",
93 | "properties": {
94 | "name": {
95 | "description": "Human readable option name",
96 | "type": "string"
97 | },
98 | "type": {
99 | "description": "Type of option",
100 | "type": "string",
101 | "enum": ["visible"]
102 | },
103 | "selector": {
104 | "description": "CSS selector",
105 | "$ref": "#/definitions/selector"
106 | },
107 | "default": {
108 | "description": "Default value",
109 | "type": "boolean"
110 | }
111 | },
112 | "required": ["name", "type", "selector"]
113 | },
114 |
115 | "colorOption": {
116 | "$id": "#colorOption",
117 | "type": "object",
118 | "properties": {
119 | "name": {
120 | "description": "Human readable option name",
121 | "type": "string"
122 | },
123 | "type": {
124 | "description": "Type of option",
125 | "type": "string",
126 | "enum": ["color"]
127 | },
128 | "material": {
129 | "description": "Material name",
130 | "type": ["string", "array"]
131 | },
132 | "options": {
133 | "description": "List of predefined colors",
134 | "type": "array",
135 | "items": { "$ref": "#/definitions/namedColor" }
136 | },
137 | "default": {
138 | "description": "Default color (hexadecimal RGB)",
139 | "type": "string"
140 | }
141 | },
142 | "required": ["name", "type", "material"]
143 | },
144 |
145 | "selectOption": {
146 | "$id": "#selectOption",
147 | "type": "object",
148 | "properties": {
149 | "name": {
150 | "description": "Human readable option name",
151 | "type": "string"
152 | },
153 | "type": {
154 | "description": "Type of option",
155 | "type": "string",
156 | "enum": ["select"]
157 | },
158 | "options": {
159 | "description": "List of predefined model parts",
160 | "type": "array",
161 | "items": {
162 | "anyOf": [{ "$ref": "#/definitions/modelPart" }]
163 | }
164 | },
165 | "default": {
166 | "description": "Default value index",
167 | "type": "integer"
168 | }
169 | },
170 | "required": ["name", "type"]
171 | },
172 |
173 | "textureOption": {
174 | "$id": "#textureOption",
175 | "type": "object",
176 | "properties": {
177 | "name": {
178 | "description": "Human readable option name",
179 | "type": "string"
180 | },
181 | "type": {
182 | "description": "Type of option",
183 | "type": "string",
184 | "enum": ["texture"]
185 | },
186 | "material": {
187 | "description": "Material name",
188 | "type": ["string", "array"]
189 | },
190 | "channels": {
191 | "description": "Material channels",
192 | "type": ["string", "array"]
193 | },
194 | "options": {
195 | "description": "List of predefined textures",
196 | "type": "array",
197 | "items": {
198 | "anyOf": [{ "$ref": "#/definitions/predefinedTexture" }]
199 | }
200 | },
201 | "default": {
202 | "description": "Default value index",
203 | "type": "integer"
204 | }
205 | },
206 | "required": ["name", "type"]
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/Viewer.js:
--------------------------------------------------------------------------------
1 | import { srgbToLinear, hexToRgb } from './lib/Colors';
2 |
3 | /**
4 | * View for the Viewer
5 | */
6 | class Viewer {
7 | /**
8 | * @param {object} iframe iframe DOM element for the viewer
9 | * @param {string} uid Model UID
10 | * @param {function} callback Callback for when the viewer is ready
11 | * @param {object} options Options
12 | * @param {object} options.params Embed params for the viewer
13 | */
14 | constructor(iframe, uid, callback, options) {
15 | this.iframe = iframe;
16 | this.uid = uid;
17 | this.callback = callback;
18 | this.options = options ? options : {};
19 | this.api = null;
20 | this.doc = null;
21 | this.materials = null;
22 | this.textures = {};
23 | this.start();
24 | }
25 |
26 | /**
27 | * Starts the viewer
28 | */
29 | start() {
30 | const client = new Sketchfab(this.iframe);
31 |
32 | const defaultParams = {
33 | graph_optimizer: 0
34 | };
35 | const userParams = this.options.hasOwnProperty('params') ? this.options.params : {};
36 | const params = Object.assign({}, defaultParams, userParams, {
37 | success: this._onSuccess.bind(this),
38 | error: this._onError.bind(this)
39 | });
40 | client.init(this.uid, params);
41 | }
42 |
43 | /**
44 | * Disposes the viewer
45 | */
46 | dispose() {
47 | this.materials = null;
48 | this.doc = null;
49 | this.api = null;
50 | this.callback = null;
51 | this.uid = null;
52 | this.iframe.src = 'about:blank';
53 | this.iframe.className = this.iframe.className
54 | .replace('js-ready', '')
55 | .replace('js-started', '');
56 | this.iframe = null;
57 | }
58 |
59 | /**
60 | * Returns the api
61 | * @return {Object} api
62 | */
63 | getApi() {
64 | return this.api;
65 | }
66 |
67 | _onSuccess(api) {
68 | this.api = api;
69 | api.start(() => {
70 | this.iframe.className += ' js-started';
71 | });
72 | api.addEventListener(
73 | 'viewerready',
74 | function() {
75 | this._onViewerReady()
76 | .then(
77 | function() {
78 | console.log('Viewer ready');
79 | this.iframe.className += ' js-ready';
80 | this.callback();
81 | }.bind(this)
82 | )
83 | .catch(function(error) {
84 | console.error(error);
85 | });
86 | }.bind(this)
87 | );
88 | }
89 |
90 | _onError() {
91 | console.error('Viewer error');
92 | }
93 |
94 | _onViewerReady() {
95 | const promises = [this._getGraph(), this._getMaterials()];
96 | return Promise.all(promises)
97 | .then(
98 | function(results) {
99 | this.doc = results[0];
100 | this.materials = results[1];
101 | console.info('Graph', results[0]);
102 | console.info('Materials', results[1]);
103 | }.bind(this)
104 | )
105 | .catch(function(error) {
106 | console.error(error);
107 | });
108 | }
109 |
110 | _getGraph() {
111 | if (!this.api) {
112 | Promise.reject('getGraph: API not ready');
113 | }
114 |
115 | return new Promise(
116 | function(resolve, reject) {
117 | this.api.getSceneGraph(
118 | function(err, result) {
119 | if (err) {
120 | return reject(err);
121 | }
122 | const doc = document.implementation.createDocument('', '', null);
123 | doc.appendChild(this._renderGraphNode(doc, result));
124 | resolve(doc);
125 | }.bind(this)
126 | );
127 | }.bind(this)
128 | );
129 | }
130 |
131 | _getMaterials() {
132 | if (!this.api) {
133 | Promise.reject('getMaterials: API not ready');
134 | }
135 |
136 | return new Promise(
137 | function(resolve, reject) {
138 | this.api.getMaterialList(function(err, materials) {
139 | if (err) {
140 | return reject(err);
141 | }
142 | resolve(materials);
143 | });
144 | }.bind(this)
145 | );
146 | }
147 |
148 | _getMaterialByName(materialName) {
149 | if (!this.materials) {
150 | return null;
151 | }
152 |
153 | return this.materials.reduce(function(acc, cur) {
154 | if (cur.name === materialName) {
155 | return cur;
156 | }
157 | return acc;
158 | }, null);
159 | }
160 |
161 | _renderGraphNode(doc, node) {
162 | var newNode = doc.createElement(node.type);
163 | newNode.setAttribute('instance', node.instanceID);
164 | if (node.name) {
165 | newNode.setAttribute('name', node.name);
166 | }
167 |
168 | if (node.children && node.children.length) {
169 | for (var i = 0, l = node.children.length; i < l; i++) {
170 | newNode.appendChild(this._renderGraphNode(doc, node.children[i]));
171 | }
172 | }
173 | return newNode;
174 | }
175 |
176 | _getInstanceIDsFromSelector(selector) {
177 | const nodes = Array.from(this.doc.querySelectorAll(selector));
178 | const ids = nodes.map(function(node) {
179 | return node.getAttribute('instance');
180 | });
181 | return ids;
182 | }
183 |
184 | /**
185 | * Shows objects targeted by selector
186 | * @param {String} selector CSS Selector for object to show
187 | */
188 | show(selector) {
189 | if (!this.api) {
190 | console.error('show: viewer not ready');
191 | return;
192 | }
193 | const ids = this._getInstanceIDsFromSelector(selector);
194 | ids.forEach(
195 | function(instanceId) {
196 | this.api.show(instanceId);
197 | }.bind(this)
198 | );
199 | }
200 |
201 | /**
202 | * Hides objects targeted by selector
203 | * @param {string} selector CSS Selector for object to hide
204 | */
205 | hide(selector) {
206 | if (!this.api) {
207 | console.error('hide: viewer not ready');
208 | return;
209 | }
210 | const ids = this._getInstanceIDsFromSelector(selector);
211 | ids.forEach(
212 | function(instanceId) {
213 | this.api.hide(instanceId);
214 | }.bind(this)
215 | );
216 | }
217 |
218 | /**
219 | * Sets color (Diffuse, DiffusePBR, AlbedoPBr) for given material name
220 | * @param {string|string[]} material Name of material. Also accepts array of names for changing multiple materials at once.
221 | * @param {string} hexColor Hex color
222 | */
223 | setColor(material, hexColor) {
224 | if (!this.api) {
225 | console.error('setColor: viewer not ready');
226 | return;
227 | }
228 |
229 | if (!Array.isArray(material)) {
230 | material = [material];
231 | }
232 |
233 | material.forEach(mat => {
234 | this._setMaterialColor(mat, hexColor);
235 | });
236 | }
237 |
238 | _setMaterialColor(materialName, hexColor) {
239 | let material = this._getMaterialByName(materialName);
240 | const linearColor = srgbToLinear(hexToRgb(hexColor));
241 | material.channels.AlbedoPBR.color = linearColor;
242 | material.channels.DiffusePBR.color = linearColor;
243 | material.channels.DiffuseColor.color = linearColor;
244 | material.channels.AlbedoPBR.texture = undefined;
245 | material.channels.DiffusePBR.texture = undefined;
246 | material.channels.DiffuseColor.texture = undefined;
247 |
248 | this.api.setMaterial(material, function(err, result) {
249 | if (err) {
250 | console.error(err);
251 | }
252 | });
253 | }
254 |
255 | /**
256 | * Sets texture on material/channels
257 | * @param {string|string[]} material Name of material. Also accepts array of material names.
258 | * @param {string|string[]} channels Name of channel. Also accepts array of channel names.
259 | * @param {string} url URL of the texture.
260 | */
261 | setTexture(material, channels, url) {
262 | if (!Array.isArray(material)) {
263 | material = [material];
264 | }
265 |
266 | material.forEach(mat => {
267 | this._setMaterialTexture(mat, channels, url);
268 | });
269 | }
270 |
271 | _setMaterialTexture(materialName, channels, url) {
272 | let material = this._getMaterialByName(materialName);
273 | const texturePromise = this._addTexture(url);
274 | texturePromise.then(textureUid => {
275 | // Accept array of channel names, or a single channel name
276 | if (!Array.isArray(channels)) {
277 | channels = [channels];
278 | }
279 | for (var i = 0; i < channels.length; i++) {
280 | if (
281 | material.channels.hasOwnProperty(channels[i]) &&
282 | material.channels[channels[i]].texture
283 | ) {
284 | // Update texture UID
285 | material.channels[channels[i]].texture.uid = textureUid;
286 | } else {
287 | // Create new texture
288 | material.channels[channels[i]].texture = {
289 | internalFormat: 'RGB',
290 | magFilter: 'LINEAR',
291 | minFilter: 'LINEAR_MIPMAP_LINEAR',
292 | texCoordUnit: 0,
293 | textureTarget: 'TEXTURE_2D',
294 | uid: textureUid,
295 | wrapS: 'REPEAT',
296 | wrapT: 'REPEAT'
297 | };
298 | }
299 | }
300 | this.api.setMaterial(material, function(err, result) {
301 | if (err) {
302 | console.error(err);
303 | }
304 | });
305 | });
306 | }
307 |
308 | _addTexture(url) {
309 | return new Promise((resolve, reject) => {
310 | if (this.textures.hasOwnProperty(url)) {
311 | resolve(this.textures[url]);
312 | } else {
313 | this.api.addTexture(url, (err, textureUid) => {
314 | if (err) {
315 | reject(err);
316 | } else {
317 | this.textures[url] = textureUid;
318 | resolve(textureUid);
319 | }
320 | });
321 | }
322 | });
323 | }
324 | }
325 |
326 | export default Viewer;
327 |
--------------------------------------------------------------------------------
/dist/SketchfabConfigurator-1.0.2.js:
--------------------------------------------------------------------------------
1 | var SketchfabConfigurator=function(t){var n={};function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}return e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.r=function(t){Object.defineProperty(t,"__esModule",{value:!0})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=209)}([function(t,n,e){var r=e(2),i=e(35),o=e(10),u=e(18),c=e(16),a=function(t,n,e){var s,f,l,h,p=t&a.F,v=t&a.G,d=t&a.S,y=t&a.P,g=t&a.B,m=v?r:d?r[n]||(r[n]={}):(r[n]||{}).prototype,w=v?i:i[n]||(i[n]={}),b=w.prototype||(w.prototype={});for(s in v&&(e=n),e)l=((f=!p&&m&&void 0!==m[s])?m:e)[s],h=g&&f?c(l,r):y&&"function"==typeof l?c(Function.call,l):l,m&&u(m,s,l,t&a.U),w[s]!=l&&o(w,s,h),y&&b[s]!=l&&(b[s]=l)};r.core=i,a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,n){var e=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},function(t,n,e){var r=e(1);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,n,e){var r=e(62)("wks"),i=e(23),o=e(2).Symbol,u="function"==typeof o;(t.exports=function(t){return r[t]||(r[t]=u&&o[t]||(u?o:i)("Symbol."+t))}).store=r},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n,e){var r=e(3),i=e(91),o=e(40),u=Object.defineProperty;n.f=e(8)?Object.defineProperty:function(t,n,e){if(r(t),n=o(n,!0),r(e),i)try{return u(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(t[n]=e.value),t}},function(t,n,e){var r=e(21),i=Math.min;t.exports=function(t){return t>0?i(r(t),9007199254740991):0}},function(t,n,e){t.exports=!e(5)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n,e){var r=e(6),i=e(24);t.exports=e(8)?function(t,n,e){return r.f(t,n,i(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n,e){var r=e(0),i=e(35),o=e(5);t.exports=function(t,n){var e=(i.Object||{})[t]||Object[t],u={};u[t]=n(e),r(r.S+r.F*o(function(){e(1)}),"Object",u)}},function(t,n,e){var r=e(65),i=e(20);t.exports=function(t){return r(i(t))}},function(t,n,e){var r=e(36),i=e(24),o=e(12),u=e(40),c=e(9),a=e(91),s=Object.getOwnPropertyDescriptor;n.f=e(8)?s:function(t,n){if(t=o(t),n=u(n,!0),a)try{return s(t,n)}catch(t){}if(c(t,n))return i(!r.f.call(t,n),t[n])}},function(t,n,e){"use strict";if(e(8)){var r=e(34),i=e(2),o=e(5),u=e(0),c=e(66),a=e(90),s=e(16),f=e(32),l=e(24),h=e(10),p=e(33),v=e(21),d=e(7),y=e(89),g=e(30),m=e(40),w=e(9),b=e(58),_=e(1),x=e(15),S=e(57),O=e(38),E=e(27),M=e(39).f,P=e(56),j=e(23),L=e(4),A=e(37),F=e(64),T=e(59),I=e(55),k=e(28),C=e(45),N=e(46),R=e(60),V=e(83),W=e(6),D=e(13),B=W.f,G=D.f,U=i.RangeError,z=i.TypeError,H=i.Uint8Array,q=Array.prototype,Y=a.ArrayBuffer,K=a.DataView,J=A(0),X=A(2),$=A(3),Q=A(4),Z=A(5),tt=A(6),nt=F(!0),et=F(!1),rt=I.values,it=I.keys,ot=I.entries,ut=q.lastIndexOf,ct=q.reduce,at=q.reduceRight,st=q.join,ft=q.sort,lt=q.slice,ht=q.toString,pt=q.toLocaleString,vt=L("iterator"),dt=L("toStringTag"),yt=j("typed_constructor"),gt=j("def_constructor"),mt=c.CONSTR,wt=c.TYPED,bt=c.VIEW,_t=A(1,function(t,n){return Mt(T(t,t[gt]),n)}),xt=o(function(){return 1===new H(new Uint16Array([1]).buffer)[0]}),St=!!H&&!!H.prototype.set&&o(function(){new H(1).set({})}),Ot=function(t,n){var e=v(t);if(e<0||e%n)throw U("Wrong offset!");return e},Et=function(t){if(_(t)&&wt in t)return t;throw z(t+" is not a typed array!")},Mt=function(t,n){if(!(_(t)&&yt in t))throw z("It is not a typed array constructor!");return new t(n)},Pt=function(t,n){return jt(T(t,t[gt]),n)},jt=function(t,n){for(var e=0,r=n.length,i=Mt(t,r);r>e;)i[e]=n[e++];return i},Lt=function(t,n,e){B(t,n,{get:function(){return this._d[e]}})},At=function(t){var n,e,r,i,o,u,c=x(t),a=arguments.length,f=a>1?arguments[1]:void 0,l=void 0!==f,h=P(c);if(void 0!=h&&!S(h)){for(u=h.call(c),r=[],n=0;!(o=u.next()).done;n++)r.push(o.value);c=r}for(l&&a>2&&(f=s(f,arguments[2],2)),n=0,e=d(c.length),i=Mt(this,e);e>n;n++)i[n]=l?f(c[n],n):c[n];return i},Ft=function(){for(var t=0,n=arguments.length,e=Mt(this,n);n>t;)e[t]=arguments[t++];return e},Tt=!!H&&o(function(){pt.call(new H(1))}),It=function(){return pt.apply(Tt?lt.call(Et(this)):Et(this),arguments)},kt={copyWithin:function(t,n){return V.call(Et(this),t,n,arguments.length>2?arguments[2]:void 0)},every:function(t){return Q(Et(this),t,arguments.length>1?arguments[1]:void 0)},fill:function(t){return R.apply(Et(this),arguments)},filter:function(t){return Pt(this,X(Et(this),t,arguments.length>1?arguments[1]:void 0))},find:function(t){return Z(Et(this),t,arguments.length>1?arguments[1]:void 0)},findIndex:function(t){return tt(Et(this),t,arguments.length>1?arguments[1]:void 0)},forEach:function(t){J(Et(this),t,arguments.length>1?arguments[1]:void 0)},indexOf:function(t){return et(Et(this),t,arguments.length>1?arguments[1]:void 0)},includes:function(t){return nt(Et(this),t,arguments.length>1?arguments[1]:void 0)},join:function(t){return st.apply(Et(this),arguments)},lastIndexOf:function(t){return ut.apply(Et(this),arguments)},map:function(t){return _t(Et(this),t,arguments.length>1?arguments[1]:void 0)},reduce:function(t){return ct.apply(Et(this),arguments)},reduceRight:function(t){return at.apply(Et(this),arguments)},reverse:function(){for(var t,n=Et(this).length,e=Math.floor(n/2),r=0;r1?arguments[1]:void 0)},sort:function(t){return ft.call(Et(this),t)},subarray:function(t,n){var e=Et(this),r=e.length,i=g(t,r);return new(T(e,e[gt]))(e.buffer,e.byteOffset+i*e.BYTES_PER_ELEMENT,d((void 0===n?r:g(n,r))-i))}},Ct=function(t,n){return Pt(this,lt.call(Et(this),t,n))},Nt=function(t){Et(this);var n=Ot(arguments[1],1),e=this.length,r=x(t),i=d(r.length),o=0;if(i+n>e)throw U("Wrong length!");for(;o255?255:255&r),i.v[p](e*n+i.o,r,xt)}(this,e,t)},enumerable:!0})};w?(v=e(function(t,e,r,i){f(t,v,s,"_d");var o,u,c,a,l=0,p=0;if(_(e)){if(!(e instanceof Y||"ArrayBuffer"==(a=b(e))||"SharedArrayBuffer"==a))return wt in e?jt(v,e):At.call(v,e);o=e,p=Ot(r,n);var g=e.byteLength;if(void 0===i){if(g%n)throw U("Wrong length!");if((u=g-p)<0)throw U("Wrong length!")}else if((u=d(i)*n)+p>g)throw U("Wrong length!");c=u/n}else c=y(e),o=new Y(u=c*n);for(h(t,"_d",{b:o,o:p,l:u,e:c,v:new K(o)});l0?r:e)(t)}},function(t,n){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++e+r).toString(36))}},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(1);t.exports=function(t,n){if(!r(t)||t._t!==n)throw TypeError("Incompatible receiver, "+n+" required!");return t}},function(t,n,e){var r=e(4)("unscopables"),i=Array.prototype;void 0==i[r]&&e(10)(i,r,{}),t.exports=function(t){i[r][t]=!0}},function(t,n,e){var r=e(9),i=e(15),o=e(63)("IE_PROTO"),u=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=i(t),r(t,o)?t[o]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?u:null}},function(t,n){t.exports={}},function(t,n,e){var r=e(6).f,i=e(9),o=e(4)("toStringTag");t.exports=function(t,n,e){t&&!i(t=e?t:t.prototype,o)&&r(t,o,{configurable:!0,value:n})}},function(t,n,e){var r=e(21),i=Math.max,o=Math.min;t.exports=function(t,n){return(t=r(t))<0?i(t+n,0):o(t,n)}},function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},function(t,n){t.exports=function(t,n,e,r){if(!(t instanceof n)||void 0!==r&&r in t)throw TypeError(e+": incorrect invocation!");return t}},function(t,n,e){var r=e(18);t.exports=function(t,n,e){for(var i in n)r(t,i,n[i],e);return t}},function(t,n){t.exports=!1},function(t,n){var e=t.exports={version:"2.5.4"};"number"==typeof __e&&(__e=e)},function(t,n){n.f={}.propertyIsEnumerable},function(t,n,e){var r=e(16),i=e(65),o=e(15),u=e(7),c=e(204);t.exports=function(t,n){var e=1==t,a=2==t,s=3==t,f=4==t,l=6==t,h=5==t||l,p=n||c;return function(n,c,v){for(var d,y,g=o(n),m=i(g),w=r(c,v,3),b=u(m.length),_=0,x=e?p(n,b):a?p(n,0):void 0;b>_;_++)if((h||_ in m)&&(y=w(d=m[_],_,g),t))if(e)x[_]=y;else if(y)switch(t){case 3:return!0;case 5:return d;case 6:return _;case 2:x.push(d)}else if(f)return!1;return l?-1:s||f?f:x}}},function(t,n,e){var r=e(3),i=e(205),o=e(61),u=e(63)("IE_PROTO"),c=function(){},a=function(){var t,n=e(67)("iframe"),r=o.length;for(n.style.display="none",e(87).appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write("