diff --git a/src/components/custom-colors.js b/src/components/custom-colors.js new file mode 100644 index 000000000..f053a4424 --- /dev/null +++ b/src/components/custom-colors.js @@ -0,0 +1,66 @@ +/* global AFRAME */ +import { getMaterials } from '../editor/components/components/CustomizeColorWidget'; +const styleParser = AFRAME.utils.styleParser; + +AFRAME.registerComponent('custom-colors', { + schema: { + type: 'string', + parse: styleParser.parse, + stringify: styleParser.stringify + }, + update() { + // If the mesh has not been traversed, duplicate the materials so that we can avoid + // accidental shared references, i.e. changing one material changes materials across multiple entities + if (!this.hasOrigColor) { + const materialMap = new Map(); + this.el.object3D.traverse((node) => { + if (node.material) { + if (!materialMap.has(node.material.uuid)) { + materialMap.set(node.material.uuid, node.material.clone()); + } + node.material = materialMap.get(node.material.uuid); + } + }); + } + + const materials = getMaterials(this.el.object3D); + materials.forEach((material) => { + if (!material.userData.origColor) { + material.userData.origColor = material.color.clone(); + this.hasOrigColor = true; + } + if (this.data[material.name] !== undefined) { + material.color.set(this.data[material.name]); + } else { + // Reset to original + material.color.set(material.userData.origColor); + } + }); + }, + updateMaterials() { + this.update(); + }, + resetAndUpdateMaterials() { + this.hasOrigColor = false; + this.updateMaterials(); + }, + init() { + this.hasOrigColor = false; + this.resetAndUpdateMaterials = this.resetAndUpdateMaterials.bind(this); + + // Models that are components of larger models trigger this event instead of model-loaded. + // This also will fire when the selected model is changed. + this.el.addEventListener('object3dset', this.resetAndUpdateMaterials); + if (this.el.getObject3D('mesh')) { + this.update(); + } + }, + remove() { + this.el.removeEventListener('object3dset', this.resetAndUpdateMaterials); + const materials = getMaterials(this.el.object3D); + materials.forEach((material) => { + // Reset to original + material.color.set(material.userData.origColor); + }); + } +}); diff --git a/src/editor/components/components/CommonComponents.js b/src/editor/components/components/CommonComponents.js index 2fcee3848..b40c459a1 100644 --- a/src/editor/components/components/CommonComponents.js +++ b/src/editor/components/components/CommonComponents.js @@ -6,6 +6,7 @@ import { getEntityClipboardRepresentation } from '../../lib/entity'; import Events from '../../lib/Events'; import Clipboard from 'clipboard'; import { saveBlob } from '../../lib/utils'; +import CustomizeColorWidget from './CustomizeColorWidget'; export default class CommonComponents extends React.Component { static propTypes = { @@ -46,7 +47,7 @@ export default class CommonComponents extends React.Component { renderCommonAttributes() { const entity = this.props.entity; // return ['position', 'rotation', 'scale', 'visible'] - return ['position', 'rotation', 'scale'].map((componentName) => { + const rows = ['position', 'rotation', 'scale'].map((componentName) => { // if entity has managed-street component, then don't show scale if (componentName === 'scale' && entity.components['managed-street']) { return null; @@ -72,6 +73,12 @@ export default class CommonComponents extends React.Component { /> ); }); + + // Custom colors are only applicable to entities, not things like intersections or groups. + if (entity.hasAttribute('mixin')) { + rows.push(); + } + return rows; } exportToGLTF() { diff --git a/src/editor/components/components/CustomizeColorWidget/index.js b/src/editor/components/components/CustomizeColorWidget/index.js new file mode 100644 index 000000000..74fffcd19 --- /dev/null +++ b/src/editor/components/components/CustomizeColorWidget/index.js @@ -0,0 +1,147 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Button } from '../Button'; +import BooleanWidget from '../../widgets/BooleanWidget'; +import ColorWidget from '../../widgets/ColorWidget'; +import SelectWidget from '../../widgets/SelectWidget'; + +export const getMaterials = (object3D) => { + const materials = new Set(); + object3D.traverse((c) => c.material && materials.add(c.material)); + return Array.from(materials); +}; + +const CustomizeColorContent = ({ materials, entity }) => { + const [colorMapping, setColorMapping] = useState( + entity.getAttribute('custom-colors') ?? {} + ); + const [selectedMaterial, setSelectedMaterial] = useState(); + + const setMaterialColor = (material, color) => { + const newColorMapping = { ...colorMapping, [material]: color }; + if (color === undefined) delete newColorMapping[material]; + setColorMapping(newColorMapping); + AFRAME.INSPECTOR.execute('entityupdate', { + entity: entity, + component: 'custom-colors', + value: newColorMapping + }); + }; + + const handleToggleOverride = (_, v) => { + setMaterialColor(selectedMaterial, v ? '#ffffff' : undefined); + }; + + const handleColorChange = (_, v) => { + setMaterialColor(selectedMaterial, v); + }; + + return ( +
+
+ + { + setSelectedMaterial(v); + }} + options={materials.map((m) => m.name)} + /> +
+ {selectedMaterial && ( + <> +
+ + +
+
+ + +
+ + )} +
+ ); +}; + +const CustomizeColorWrapper = ({ entity }) => { + const [hasCustomColorComponent, setHasCustomColorComponent] = useState( + Boolean(entity.getAttribute('custom-colors')) + ); + + const toggleCustomColors = () => { + if (!hasCustomColorComponent) { + AFRAME.INSPECTOR.execute('componentadd', { + entity, + component: 'custom-colors', + value: '' + }); + setHasCustomColorComponent(true); + return; + } + AFRAME.INSPECTOR.execute('componentremove', { + entity, + component: 'custom-colors' + }); + setHasCustomColorComponent(false); + }; + + const [materials, setMaterials] = useState([]); + + const updateMaterials = useCallback(() => { + // Save the original material color values + const newMaterials = getMaterials(entity.object3D); + setMaterials(newMaterials); + }, [entity.object3D]); + + // We need to dynamically get the materials from the mesh in case the + // model is not loaded when the pane is loaded + useEffect(() => { + entity.addEventListener('object3dset', updateMaterials); + if (entity.getObject3D('mesh')) { + updateMaterials(); + } else { + entity.addEventListener('model-loaded', updateMaterials, { + once: true + }); + } + return () => { + entity.removeEventListener('object3dset', updateMaterials); + }; + }, [updateMaterials, entity.id, entity]); + + // No materials to customize, don't add the widget + if (materials.length === 0) { + return <>; + } + + return ( +
+
+ + +
+ {hasCustomColorComponent && ( + + )} +
+ ); +}; + +export default CustomizeColorWrapper; diff --git a/src/index.js b/src/index.js index 42a65780f..908166ba4 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ require('./components/svg-extruder.js'); require('./lib/animation-mixer.js'); require('./lib/aframe-gaussian-splatting-component.min.js'); require('./assets.js'); +require('./components/custom-colors.js'); require('./components/notify.js'); require('./components/create-from-json'); require('./components/screentock.js');