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
47 changes: 47 additions & 0 deletions src/components/custom-colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* global AFRAME */

import { getMaterials } from '../editor/components/components/CustomizeColorWidget';

AFRAME.registerComponent('custom-colors', {
schema: {
type: 'string'
},
update() {
const materials = getMaterials(this.el.object3D);
const customColorMapping = {};
this.data
.replaceAll(' ', '')
.split(';')
.forEach((entry) => {
// Skip unnamed
if (entry === '') return;
const [mat, color] = entry.split(':');
customColorMapping[mat] = color;
});
kfarr marked this conversation as resolved.
Show resolved Hide resolved

materials.forEach((material) => {
if (customColorMapping[material.name] !== undefined) {
material.color.set(customColorMapping[material.name]);
} else {
// Reset to default, no tint
material.color.set(material.userData.origColor);
}
});
},
init() {
if (this.el.getObject3D('mesh')) {
this.update();
} else {
this.el.addEventListener('model-loaded', () => {
this.update();
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Properly unregister this model-loaded listener in remove.

  updateMaterials() {
    this.update():
  },
  init() {
    this.updateMaterials = this.updateMaterials.bind(this);
    if (this.el.getObject3D('mesh')) {
      this.update();
    } else {
      this.el.addEventListener('model-loaded', this.updateMaterials);
    }
  },
  remove() {
    this.el.removeEventListener('model-loaded', this.updateMaterials);
    ...
  }

Copy link
Collaborator

Choose a reason for hiding this comment

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

Move the this.updateMaterials = this.updateMaterials.bind(this); at the beginning. Listening to model-loaded shouldn't be needed anymore if you listen on object3dset. object3dset is fired just before model-loaded https://github.com/aframevr/aframe/blob/cc43ea67f8170d8ca7e5bdcbaade031df103801e/src/components/gltf-model.js#L48-L49

}
},
remove() {
const materials = getMaterials(this.el.object3D);
materials.forEach((material) => {
// Reset to default, no tint
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) => {
const schema = AFRAME.components[componentName].schema;
var data = entity.object3D[componentName];
if (componentName === 'rotation') {
Expand All @@ -68,6 +69,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
169 changes: 169 additions & 0 deletions src/editor/components/components/CustomizeColorWidget/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useMemo, 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 customColorData = entity.getAttribute('custom-colors') ?? '';
// Convert the string data of `materialName:color;...` to a mapping of color overrides: { [materialName]: color }
const baseColorMapping = useMemo(() => {
if (!customColorData) return {};
const mapping = {};
customColorData
.replaceAll(' ', '')
.split(';')
.forEach((entry) => {
// Skip unnamed
if (entry === '') return;
const [mat, color] = entry.split(':');
mapping[mat] = color;
});
return mapping;
}, [customColorData]);
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 remove all this and use

const baseColorMapping = entity.getAttribute('custom-colors') ?? {}

after you redefined the custom-colors schema to use the custom parse/stringify.

const [colorMapping, setColorMapping] = useState(baseColorMapping);
const [selectedMaterial, setSelectedMaterial] = useState();

const setMaterialColor = (material, color) => {
const newColorMapping = { ...colorMapping, [material]: color };
if (color === undefined) delete newColorMapping[material];
setColorMapping(newColorMapping);

const newColorsString = Object.entries(newColorMapping)
.map(([mat, color]) => `${mat}:${color}`)
.join(';');
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need to serialize yourself, just give the object to execute.


AFRAME.INSPECTOR.execute('entityupdate', {
entity: entity,
component: 'custom-colors',
value: newColorsString
});
};

const handleToggleOverride = (_, v) => {
if (v) {
setMaterialColor(selectedMaterial, '#FFFFFF');
} else {
setMaterialColor(selectedMaterial, 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);
newMaterials.forEach((material) => {
material.userData.origColor = material.color.clone();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be done in the aframe component, not here. There is probably an issue here, it will override material.userData.origColor with the current customized color if you select another car and back this car because the callback will be recreated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense. I changed it to be initialized once whenever the component is added. Since this presents an issue if the model is changed, I will make it so that changing the model will remove the custom-colors component. This makes most sense, as most models will have unique materials

});
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(() => {
if (entity.getObject3D('mesh')) {
updateMaterials();
} else {
entity.addEventListener('model-loaded', () => {
updateMaterials();
});
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.

}
}, [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 @@ -12,6 +12,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