Change material of gltf-imported mesh programmatically - react three fiber - reactjs

I'm currently working at a project with react-three-fiber. I've imported the model with useGLTF from #react-three/drei.
With
const { nodes, materials } = useGLTF('/model.glb');
I access the materials from the glb-file.
To access and manipulate the model I used gltfjsx to generate the model.
Now I need to change the material of a mesh programmatically. Because I have no direct access to the JSX of the model I do it with React.cloneElement and modify the props of the mesh.
So I tried something like this:
return React.cloneElement(mesh, {
material: overwriteMaterial ?
<meshStandardMaterial attach="material" color={0xa3005c} metalness={1} roughness={0.2} visible={true} /> :
materials['mat_7']
});
If overwriteMaterial is false it works. It shows the material it should. But if it's true then the mesh disappears.
I also thought of putting the <meshStandardMaterial /> in the children prop of the mesh. Something like so:
return React.cloneElement(mesh, {
material: overwriteMaterial ? undefined : materials['mat_7'],
children: overwriteMaterial ? <meshStandardMaterial attach="material" color={0xa3005c} metalness={1} roughness={0.2} visible={true} /> : undefined
});
With this I always get this error and I don't know why it appears:
TypeError: Cannot read properties of undefined (reading 'visible')
Could this approach somehow work or am I doing something completely wrong?
Every help is welcome. Thanks

Alright so I found the answer by myself after some more hours searching for a solution.
The material property doesn't accept a JSX-tag. So if you create an instance of the class MeshStandardMaterial you can pass it to the property and it works perfectly fine. Now it looks something like this:
return React.cloneElement(mesh, {
material: overwriteMaterial
? new MeshStandardMaterial({ color: 0x0ff000 })
: materials['mat_7']
})
Note: The class MeshStandardMaterial is exported from the three package.

i really don't think you have to clone any react element, that doesn't seem correct. you can clone or mutate materials just as you would in a plain three app. i have no idea why you even want to clone jsx.
const { scene } = useGLTF(url)
const clonedScene = useMemo(() => scene.clone(), [])
useLayoutEffect(() => {
clonedScene.traverse(o => {
if (o.type === 'Mesh') {
o.material = ...
}
})
}, [clonedScene]}
return <primitive object={clonedScene} />
you can skip the clonedScene thing completely as well, this is only if you plan to re-use the model in your scene multiple times.

Related

react native Why does my async code hang the rendering?

General :
TL;DR: async code hangs rendering.
I have this component with a Modal and inside the Modal it renders a list of filters the user can choose from. When pressing a filter the color of the item changes and it adds a simple code(Number) to an array. The problem is that the rendering of the color change hangs until the logic that adds the code to the array finishes.
I don't understand why adding a number to an array takes between a sec and two.
I don't understand why the rendering hangs until the entire logic behind is done.
Notes: I come from a Vue background and this is the first project where I'm using react/react-native. So if I'm doing something wrong it would be much appreciated if someone points that out
Snack that replicates the issue :
Snack Link
My code for reference :
I use react-native with expo managed and I use some native-base components for the UI.
I can't share the whole code source but here are the pieces of logic that contribute to the problem :
Parent : FilterModal.js
The rendering part :
...
<Modal
// style={styles.container}
visible={modalVisible}
animationType="slide"
transparent={false}
onRequestClose={() => {
this.setModalVisible(!modalVisible);
}}
>
<Center>
<Pressable
onPress={() => this.setModalVisible(!modalVisible)}
>
<Icon size="8" as={MaterialCommunityIcons} name="window-close" color="danger.500" />
</Pressable>
</Center>
// I use sectionList because the list of filters is big and takes time to render on the screen
<SectionList
style={styles.container}
sections={[
{ title: "job types", data: job_types },
{ title: "job experience", data: job_experience },
{ title: "education", data: job_formation },
{ title: "sector", data: job_secteur }
]}
keyExtractor={(item) => item.id}
renderItem={({ item, section }) => <BaseBadge
key={item.id}
pressed={this.isPressed(section.title, item.id)}
item={item.name}
code={item.id}
type={section.title}
add={this.addToFilters.bind(this)}
></BaseBadge>}
renderSectionHeader={({ section: { title } }) => (
<Heading color="darkBlue.400">{title}</Heading>
)}
/>
</Modal>
...
The logic part :
...
async addToFilters(type, code) {
switch (type) {
case "job types":
this.addToTypesSelection(code);
break;
case "job experience":
this.addToExperienceSelection(code);
break;
case "formation":
this.addToFormationSelection(code);
break;
case "sector":
this.addToSectorSelection(code);
break;
default:
//TODO
break;
}
}
...
// the add to selection methods look something like this :
async addToTypesSelection(code) {
if (this.state.jobTypesSelection.includes(code)) {
this.setState({ jobTypesSelection: this.state.jobTypesSelection.filter((item) => item != code) })
}
else {
this.setState({ jobTypesSelection: [...this.state.jobTypesSelection, code] })
}
}
...
Child :
The rendering Part
render() {
const { pressed } = this.state;
return (
< Pressable
// This is the source of the problem and read further to know why I used the setTimeout
onPress={async () => {
this.setState({ pressed: !this.state.pressed });
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
<Badge
bg={pressed ? "primary.300" : "coolGray.200"}
rounded="md"
>
<Text fontSize="md">
{this.props.item}
</Text>
</Badge>
</Pressable >
);
};
Expected outcome :
The setState({pressed:!this.state.pressed}) finishes the rendering of the item happens instantly, the rest of the code happens after and doesn't hang the rendering.
The change in the parent state using the add code to array can happen in the background but I need the filter item ui to change instantly.
Things I tried :
Async methods
I tried making the methods async and not await them so they can happen asynchronously. that didn't change anything and seems like react native ignores that the methods are async. It hangs until everything is done all the way to the method changing the parent state.
Implementing "event emit-listen logic"
This is the first app where I chose to use react/react-native, coming from Vue I got the idea of emitting an event from the child and listening to it on the parent and execute the logic that adds the code to the array.
This didn't change anything, I used eventemitter3 and react-native-event-listeners
Using Timeout
This is the last desperate thing I tried which made the app useable for now until I figure out what am I doing wrong.
basically I add a Timeout after I change the state of the filter component like so :
...
< Pressable
onPress={async () => {
// change the state this changes the color of the item ↓
this.setState({ pressed: !this.state.pressed });
// this is the desperate code to make the logic not hang the rendering ↓
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
...
Thanks for reading, helpful answers and links to the docs and other articles that can help me understand better are much appreciated.
Again I'm new to react/react-native so please if there is some concept I'm not understanding right point me in the right direction.
For anyone reading this I finally figured out what was the problem and was able to solve the issue for me.
The reason the rendering was getting hang is because the code that pushes to my array took time regardless of me making it async or not it was being executed on the main thread AND that change was triggering screen re-render which needed to wait for the js logic to finish.
The things that contribute to the solution and improvement are :
Make the array (now a map{}) that holds the selected filters stateless, in other words don't use useState to declare the array, instead use good old js which will not trigger any screen re-render. When the user applies the filters then push that plain js object to a state or context like I'm doing and consume it, doing it this way makes sure that the user can spam selecting and deselecting the filters without hanging the interactions.
first thing which is just a better way of doing what I needed is to make the array a map, this doesn't solve the rerender issue.

React Native List Component that looks like iOS native lists

Does anybody know if there is any built-in React Native component that render lists like this?
It shouldn't be super hard to implement this from scratch
If you want the exactly same design there are probably three or so components that you need to write
Heres pseudo code
const settings = [
{
items : [{
icon : SomeIcon,
label : "Notifications",
action: () => ...navigate somewhere
},
...more items
]
}
]
...more code
return
<FlatList>
{settings.map(setting =>{
return <SettingSection>
{setting.items.map(item =>{
return <Item/>
})
}
</SettingSection>
})}
</FlatList>
You can try to do it by yourself but if you want exactly the same view, here i found a npm package for you. https://www.npmjs.com/package/react-native-settings-list . it is efficient and customizable.

Accessing Full Calendar API in ReactJS

How do I access the full calendar API so that I can manipulate and use methods like setDates, moveStart etc.. I want to use the methods they provided in this page.
https://fullcalendar.io/docs/Event-setDates
Problem:
The example full calendar provided is class based and is outdated. How do I use these methods in a functional component..
export default class DemoApp extends React.Component {
calendarRef = React.createRef()
render() {
return (
<FullCalendar ref={this.calendarRef} plugins={[ dayGridPlugin ]} />
)
}
someMethod() {
let calendarApi = this.calendarRef.current.getApi()
calendarApi.next()
}
}
What I did so far Currently I use the reference inside handleEventAdd and handleUpdate functions, I take the event and manipulate it accordingly.
<FullCalendar
nowIndicator
plugins={[momentTimezonePlugin, timeGridPlugin, dayGridPlugin, interactionPlugin]}
initialView="timeGridWeek"
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay',
}}
timeZone={userTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone}
ref={calendarRef}
editable
selectable
select={handleEventAdd}
eventResize={(e) => handleupdateSchedule(e)}
/>
What works:
const CalAPI = e.view.calendar
when I do this I can access the Calendar methods like
Calendar::getEvents
Calendar::getEventById
Calendar::addEvent
I use them like this
CalAPI.getEventById(e.event.id).remove() //works
But I cannot access other methods like
Event::setProp
Event::setExtendedProp
Event::setStart
Event::setEnd
Event::setDates
The above don't work. Please find the list of the Event methods I want to use here https://fullcalendar.io/docs/Event-setDates
UPDATE:
Posting more information on the functions.
select={Addevent} // works even when I don`t pass the "e"
eventClick={(e) => Deleteevent(e)} // does not find CalAPI reference
eventDrop={(e) => Updateevent(e)} // does not find CalAPI
eventResize={(e) => Updateevent(e)} // the calendar event refreshes automatically I dont need the CalAPI here.
SOLUTION
Finally after a lot of work, the solution is quite simple actually, we are supposed to use the useRef hook when using functional components. Then access the CalAPI from yourRefname.current.getAPI()... from here on you can access all the needed methods.
PS: Thanks to #ADyson, I got closer to the answer.

Passing array as props in React Native

I have been reading a lot of threads and I couldn't find a solution to my problem.
I am passing some arguments to another React Native component in the following way:
<Footer buttonsActive={{ firstButton: 'true', secondButton: 'false' }} />
When I console log the parameter in Footer I get:
Object {
"buttonsActive": Object {
"firstButton": "true",
"secondButton": "false",
},
}
It looks fine, but when I try to console log the elements in the following way:
console.log(buttonsActive.firstButton);
I get an undefined error. What is the way to access these 2 values without iterating by using .map() and asking if (var == firstButton)....
Thanks
It may be the buttonsActive object that is not found is your context. It should be a prop inside your Footer.
Maybe try with console.log(this.props.buttonsActive.firstButton); inside the Footer.
I finally understood how it works, so one can send the parameters in this way:
<Footer firstButton secondButton={false} />
And one accesses them in the Footer component in this way:
const Footer = (buttonStatus) => {
const firstButtonColor = (buttonStatus.firstButton ? 'black' : 'grey');
const SecondButtonColor = (buttonStatus.secondButton ? 'black' : 'grey');
I hope it helps.

Creating a custom Leaflet layer control in React

I'm attempting to completely recreate or reorganize the functionality of the LayersControl component in its own separate panel using react-leaflet.
I have several filtered into their own and it works fine, but I'd like to customize the look and location of the Control element.
I've hosted the current version of my Leaflet app on github pages here. You can see the control on the right, which is the basic Leaflet control, but I'd like to the Icon on the left (the layers icon) to accomplish the same thing instead with custom react components.
Just wondering if anyone can point me in the right direction to beginning to accomplish this!
This is my current render for my react-leaflet map:
render() {
const types = [...new Set(data.map(loc => loc.type))];
const group = types.map(type =>
data.filter(loc => loc.type === type)
.map(({id, lat, lng, name}) =>
<LayersControl.Overlay name={startCase(toLower(type))}>
<LayerGroup>
<Marker key={id} position={[lat, lng]} icon=
{locationIcon}>
<Tooltip permanent direction="bottom" opacity={.6}>
{name}
</Tooltip>
</Marker>
</LayerGroup>
</LayersControl.Overlay>
));
return (
<>
<ControlPanel />
<Map
zoomControl={false}
center={this.state.center}
zoom={this.state.zoom}
maxBounds={this.state.maxBounds}
maxZoom={10}
>
<LayersControl>
<TileLayer
url='https://cartocdn-gusc.global.ssl.fastly.net//ramirocartodb/api/v1/map/named/tpl_756aec63_3adb_48b6_9d14_331c6cbc47cf/all/{z}/{x}/{y}.png'
/>
<ZoomControl position="topright" />
{group}
</LayersControl>
</Map>
</>
);
}
So theres still a few bugs in this but i've managed get most of the way (self taught react) using material UI as an example, can be seen in this sandbox link:
https://codesandbox.io/embed/competent-edison-wt5pl?fontsize=14
The general bassis is that we extend MapControl which means we have to define createLeafletElement, this has to return a generic leaflet (not react) control from the original javascript leaflet package. Essentially making a div with the domutil provided by leaflet and then portaling our react components through that div with react portals.
Again with another class extension we extend some of the classes provided by react-leaflet for layers, i pulled it out and just made a generic layer that you could define a group for, that way you could render any layer (polygon, baselayer etc) and specify the group to tell it where to go in the layer control i.e no need for specific components or overlays. As we are extending the class we need implement and pass down the methods we want to use, like addLayer, remove layer etc. During these implementations i've just added them to state to track what layers are active and such.
Not sure if there are better practices throughout everything i've implemented but this is definitely a start, hopefully in the right direction.
Bugs - The first layer in each group won't turn on correctly without the 2nd item ticked, something to do with state i think but didn't have the time to track it down
Thanks Dylan and Peter for this nice React Leaflet custom control approach. I assumed there was still a bug in the toggleLayer function. It's checked multiple checkboxes and the layers won't change properly. So I restructered a little bit and now it should work fine.
toggleLayer = layerInput => {
const { name, group } = layerInput;
let layers = { ...this.state.layers };
layers[group] = layers[group].map(l => {
l.checked = false;
this.removeLayer(l.layer);
if (l.name === name) {
l.checked = !l.checked;
this.props.leaflet.map.addLayer(l.layer);
}
return l;
});
this.setState({
layers
});
};
Just to elaborate on the bug that is mentioned in Dylans answer...
If you have more then one ControlledLayerItem, none items are added to the map until the very last item is checked. To fix this, the toggleLayer method in ControlLayer2.js has to be slightly modified:
toggleLayer = layerInput => {
const { layer, name, checked, group } = layerInput;
let layers = { ...this.state.layers };
layers[group] = layers[group].map(l => {
if (l.name === name) {
l.checked = !l.checked;
l.checked
? this.props.leaflet.map.addLayer(layer)
: this.removeLayer(layer);
}
return l;
});
this.setState({
layers
});
};
Thanks Dylan for the code, it was really helpfull.

Resources