threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
284 lines • 12.7 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { AViewerPluginSync } from '../../viewer';
import { PickingPlugin } from '../interaction/PickingPlugin';
import { imageBitmapToBase64, makeColorSvgCircle, serialize } from 'ts-browser-helpers';
import { PhysicalMaterial } from '../../core';
import { MaterialPreviewGenerator } from '../../three';
/**
* Material Configurator Plugin (Base)
*
* This plugin allows you to create variations of materials mapped to material names or uuids in the scene.
* These variations can be applied to the materials in the scene. (This copies the properties to the same material instances instead of assigning new materials)
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
*
* See `MaterialConfiguratorPlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
*
* @category Plugins
*/
export class MaterialConfiguratorBasePlugin extends AViewerPluginSync {
constructor() {
super();
this.enabled = true;
this._uiNeedRefresh = false;
/**
* Apply all variations(by selected index or first item) when a config is loaded
*/
this.applyOnLoad = true;
this.variations = [];
this._selectedMaterial = () => (this._picking?.getSelectedObject()?.material || undefined);
this.uiConfig = {
label: 'Material Configurator',
type: 'folder',
// expanded: true,
children: [
() => [
{
type: 'input',
label: 'uuid',
property: [this._selectedMaterial(), 'uuid'],
hidden: () => !this._selectedMaterial(),
disabled: true,
},
{
type: 'input',
label: 'mapping',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'uuid'],
onChange: async () => this.refreshUi(),
},
{
type: 'input',
label: 'title',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'title'],
onChange: async () => this.refreshUi(),
},
{
type: 'dropdown',
label: 'Preview Type',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'preview'],
onChange: async () => this.refreshUi(),
children: ['generate:sphere', 'generate:cube', 'color', 'map', 'emissive', ...Object.keys(PhysicalMaterial.MaterialProperties).filter(x => x.endsWith('Map'))].map(k => ({
label: k,
value: k,
})),
},
...this.getSelectedVariation()?.materials.map(m => {
return m.uiConfig ? Object.assign(m.uiConfig, { expanded: false }) : {};
}) || [],
{
type: 'button',
label: 'Clear variations',
hidden: () => !this._selectedMaterial(),
value: async () => {
const v = this.getSelectedVariation();
if (v && await this._viewer.dialog.confirm('Material configurator: Remove all variations for this material?'))
v.materials = [];
this.refreshUi();
},
},
{
type: 'button',
label: 'Remove completely',
hidden: () => !this._selectedMaterial(),
value: async () => {
const v = this.getSelectedVariation();
if (v && await this._viewer.dialog.confirm('Material configurator: Remove this variation?')) {
this.removeVariation(v);
}
},
},
{
type: 'button',
label: 'Add Variation',
hidden: () => !this._selectedMaterial(),
value: async () => {
const mat = this._selectedMaterial();
if (!mat)
return;
if (!mat.name && !await this._viewer?.dialog.confirm('Material configurator: Material has no name. Use uuid instead?'))
return;
this.addVariation(mat);
},
},
{
type: 'button',
label: 'Refresh Ui',
value: () => this.refreshUi(),
},
{
type: 'button',
label: 'Apply All',
value: () => {
this.variations.forEach(v => this.applyVariation(v, v.materials[0].uuid));
},
},
],
],
};
this.addEventListener('deserialize', this.refreshUi);
this.refreshUi = this.refreshUi.bind(this);
this._refreshUi = this._refreshUi.bind(this);
this._refreshUiConfig = this._refreshUiConfig.bind(this);
}
onAdded(viewer) {
super.onAdded(viewer);
// todo subscribe to plugin add event if picking is not added yet.
viewer.forPlugin(PickingPlugin, (p) => {
this._picking = p;
this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig);
}, () => {
this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig);
this._picking = undefined;
});
this._previewGenerator = new MaterialPreviewGenerator();
viewer.addEventListener('preFrame', this._refreshUi);
}
/**
* Reapply all selected variations again.
* Useful when the scene is loaded or changed and the variations are not applied.
*/
reapplyAll() {
this.variations.forEach(v => this.applyVariation(v, v.materials[v.selectedIndex ?? 0].uuid));
}
fromJSON(data, meta) {
this.variations = [];
if (!super.fromJSON(data, meta))
return null; // its not a promise
if (data.applyOnLoad === undefined) { // old files
this.applyOnLoad = false;
}
if (this.applyOnLoad)
this.reapplyAll();
return this;
}
onRemove(viewer) {
this._previewGenerator?.dispose();
this._previewGenerator = undefined;
this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig);
this.removeEventListener('deserialize', this.refreshUi);
viewer.removeEventListener('preFrame', this._refreshUi);
this._picking = undefined;
return super.onRemove(viewer);
}
findVariation(uuid) {
return uuid ? this.variations.find(v => v.uuid === uuid) : undefined;
}
getSelectedVariation() {
return this.findVariation(this._selectedMaterial()?.uuid) || this.findVariation(this._selectedMaterial()?.name);
}
/**
* Apply a material variation based on index or uuid.
* @param variations
* @param matUuidOrIndex
*/
applyVariation(variations, matUuidOrIndex) {
const m = this._viewer?.materialManager;
if (!m)
return false;
const material = typeof matUuidOrIndex === 'string' ?
variations.materials.find(m1 => m1.uuid === matUuidOrIndex) :
variations.materials[matUuidOrIndex];
if (!material)
return false;
variations.selectedIndex = variations.materials.indexOf(material);
return m.applyMaterial(material, variations.uuid);
}
/**
* Get the preview for a material variation
* Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
* @param preview - Type of preview. Could be generate:sphere, generate:cube, color, map, emissive, etc.
* @param material - Material or index of the material in the variation.
* @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
*/
getPreview(material, preview, viewerSetDirty = true) {
if (!this._viewer)
return '';
// const m = typeof material === 'number' ? variation.materials[material] : material
const m = material;
if (!m)
return '';
let image = '';
if (!preview.startsWith('generate:')) {
const pp = m[preview] || '#ff00ff';
image = pp.image ? imageBitmapToBase64(pp.image, 100) : '';
if (!image.length)
image = makeColorSvgCircle(pp.isColor ? pp.getHexString() : pp);
}
else {
image = this._previewGenerator.generate(m, this._viewer.renderManager.renderer, this._viewer.scene.environment, preview.split(':')[1]);
}
if (viewerSetDirty)
this._viewer.setDirty(); // because called from preFrame
return image;
}
/**
* Refreshes the UI in the next frame
*/
refreshUi() {
if (!this.enabled || !this._viewer)
return;
this._uiNeedRefresh = true;
}
_refreshUiConfig() {
if (!this.enabled)
return;
this.uiConfig.uiRefresh?.(); // don't call this.refreshUi here
}
// must be called from preFrame
async _refreshUi() {
if (!this.enabled)
return false;
if (!this._viewer || !this._uiNeedRefresh)
return false;
this._uiNeedRefresh = false;
this._refreshUiConfig();
return true;
}
removeVariationForMaterial(material) {
let variation = this.findVariation(material.uuid);
if (!variation && material.name.length > 0)
variation = this.findVariation(material.name);
if (variation)
this.removeVariation(variation);
}
removeVariation(variation) {
if (!variation)
return;
this.variations.splice(this.variations.indexOf(variation), 1);
this.refreshUi();
}
addVariation(material, variationKey, cloneMaterial = true) {
const clone = cloneMaterial && material?.clone ? material.clone() : material;
if (material && clone) {
let variation = this.findVariation(variationKey ?? material.uuid);
if (!variation && !variationKey && material.name.length > 0)
variation = this.findVariation(material.name);
if (!variation) {
variation = this.createVariation(material, variationKey);
}
variation.materials.push(clone);
this.refreshUi();
}
}
createVariation(material, variationKey) {
this.variations.push({
uuid: variationKey ?? material.name.length > 0 ? material.name : material.uuid,
title: material.name.length > 0 ? material.name : 'No Name',
preview: 'generate:sphere',
materials: [],
});
return this.variations[this.variations.length - 1];
}
}
MaterialConfiguratorBasePlugin.PluginType = 'MaterialConfiguratorPlugin';
__decorate([
serialize()
], MaterialConfiguratorBasePlugin.prototype, "variations", void 0);
//# sourceMappingURL=MaterialConfiguratorBasePlugin.js.map