Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entity Color Options #1005

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
66 changes: 66 additions & 0 deletions src/components/custom-colors.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
kfarr marked this conversation as resolved.
Show resolved Hide resolved
}

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);
});
}
});
9 changes: 8 additions & 1 deletion src/editor/components/components/CommonComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -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(<CustomizeColorWidget entity={entity} key={entity.id} />);
}
return rows;
}

exportToGLTF() {
Expand Down
147 changes: 147 additions & 0 deletions src/editor/components/components/CustomizeColorWidget/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="details">
<div className="propertyRow">
<label className="text">Material</label>
<SelectWidget
name="material"
value={selectedMaterial}
onChange={(_, v) => {
setSelectedMaterial(v);
}}
options={materials.map((m) => m.name)}
/>
</div>
{selectedMaterial && (
<>
<div className="propertyRow">
<label className="text">Override default</label>
<BooleanWidget
componentname="override"
name="override"
onChange={handleToggleOverride}
value={colorMapping[selectedMaterial] !== undefined}
/>
</div>
<div className="propertyRow">
<label className="text">Color</label>
<ColorWidget
componentname="color"
name="color"
value={colorMapping[selectedMaterial]}
onChange={handleColorChange}
/>
</div>
</>
)}
</div>
);
};

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
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add {once: true} for this listener.

}
return () => {
entity.removeEventListener('object3dset', updateMaterials);
};
}, [updateMaterials, entity.id, entity]);

// No materials to customize, don't add the widget
if (materials.length === 0) {
return <></>;
}

return (
<div className="details">
<div className="propertyRow">
<label className="text">Custom Colors</label>
<Button variant="toolbtn" onClick={toggleCustomColors}>
{hasCustomColorComponent ? 'Remove' : 'Add'} Custom Colors
</Button>
</div>
{hasCustomColorComponent && (
<CustomizeColorContent
materials={materials}
entity={entity}
key={entity.object3D}
/>
)}
</div>
);
};

export default CustomizeColorWrapper;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading