Change Material of a babylonJs model with react-babylonjs - reactjs

I load a model coming from Blender (exported with babylon js exporter). The model comes with materials. and has 5 meshes (very simple test model).
I would like to change albedo (color under natural light) of some materials, but don't get how to do, as there is no component related to the material (because imported) and in react, there is usually a function to call to update internal values (then a refresh is triggered).
const onModelLoaded = model => {
model.meshes.forEach(mesh => {
console.log(`mesh... `, mesh.material.albedoColor);
// It shows well albedo of each material
});
};
export const SceneWithLoad = () => {
return (
<div>
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Suspense>
<Model
rootUrl="assets/firstLoco.babylon"
sceneFileName=""
onModelLoaded={onModelLoaded}
/>
</Suspense>
<hemisphericLight ... />
<arcRotateCamera ... />
</Scene>
</Engine>
</div>
);
};
When mesh is loaded, I can see albedo of each material with onModelLoaded (that's great), now I would like to update albedo on a regular basis (setInterval(() => {changeAlbedo()}, 1000)), but ref to Material objects change on refresh, and I need to call a function for react to know code updated the material albedo.
Cant find the trick here, Thanks for advices !

Following numerous tests
Here the ids of the materials are known. They are used to set albedo on them through the lodash filter function. In this case, it alternates red / white lights in front / back of a locomotive.
boolBal is a value set to true or false every sec. SwapMaterial doesn't display anything and is simply an entry point for the scene modification code.
What is not really "react way of working" is that scene are mutable (you can update a scene item without generating a new reference), react in principle is not (state change = new ref to the state object)
Any better suggestion please comment.
import _ from "lodash";
import { Engine, Scene, Model, useScene } from "react-babylonjs";
import { Vector3, Color3 } from "#babylonjs/core";
import "#babylonjs/loaders";
const colorWhite = new Color3(1, 1, 1);
const colorRed = new Color3(1, 0, 0);
const SwapMaterial = ({ boolVal }) => {
const scene = useScene();
_.filter(
scene.materials,
r => ["FrontLeft-mat", "FrontRight-mat"].indexOf(r.id) >= 0
).forEach(m => {
m.albedoColor = boolVal ? colorRed : colorWhite;
});
_.filter(
scene.materials,
r => ["RearLeft-mat", "RearRight-mat"].indexOf(r.id) >= 0
).forEach(m => {
m.albedoColor = !boolVal ? colorRed : colorWhite;
});
return null;
};
export const SceneWithLoad = () => {
const [boolVal, boolValSet] = useState(false);
const ref = useRef({});
ref.current.boolVal = boolVal;
ref.current.boolValSet = boolValSet;
useEffect(() => {
const intId = setInterval(() => {
ref.current.boolValSet(!ref.current.boolVal);
}, 1000);
return () => {
clearInterval(intId);
};
}, []);
return (
<div>
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Suspense>
<Model rootUrl="assets/firstLoco.babylon" sceneFileName="" />
</Suspense>
<hemisphericLight ... />
<hemisphericLight ... />
<arcRotateCamera ... />
<SwapMaterial {...{ boolVal }} />
</Scene>
</Engine>
</div>
);
};

Related

Use context for communication between components at different level

I'm building the settings pages of my apps, in which we have a common SettingsLayout (parent component) which is rended for all the settings page. A particularity of this layout is that it contains an ActionsBar, in which the submit/save button for persisting the data lives.
However, the content of this SettingsLayout is different for each page, as every one of them has a different form and a different way to interact with it. For persisting the data to the backend, we use an Apollo Mutation, which is called in one of the child components, that's why there is no access to the ActionsBar save button.
For this implementation, I thought React Context was the most appropriated approach. At the beginning, I thought of using a Ref, which was updated with the submit handler function in each different render to be aware of the changes.
I've implemented a codesandbox with a very small and reduced app example to try to illustrate and clarify better what I try to implement.
https://codesandbox.io/s/romantic-tdd-y8tpj8?file=/src/App.tsx
Is there any caveat with this approach?
import React from "react";
import "./styles.css";
type State = {
onSubmit?: React.MutableRefObject<() => void>;
};
type SettingsContextProviderProps = {
children: React.ReactNode;
value?: State;
};
type ContextType = State;
const SettingsContext = React.createContext<ContextType | undefined>(undefined);
export const SettingsContextProvider: React.FC<SettingsContextProviderProps> = ({
children
}) => {
const onSubmit = React.useRef(() => {});
return (
<SettingsContext.Provider value={{ onSubmit }}>
{children}
</SettingsContext.Provider>
);
};
export const useSettingsContext = (): ContextType => {
const context = React.useContext(SettingsContext);
if (typeof context === "undefined") {
/*throw new Error(
"useSettingsContext must be used within a SettingsContextProvider"
);*/
return {};
}
return context;
};
function ExampleForm() {
const { onSubmit } = useSettingsContext();
const [input1, setInput1] = React.useState("");
const [input2, setInput2] = React.useState("");
onSubmit.current = () => {
console.log({ input1, input2 });
};
return (
<div className="exampleForm">
<input
placeholder="Input 1"
onChange={(event) => setInput1(event.target.value)}
/>
<input
placeholder="Input 2"
onChange={(event) => setInput2(event.target.value)}
/>
</div>
);
}
function ActionsBar() {
const { onSubmit } = useSettingsContext();
return (
<section className="actionsBar">
<strong>SETTINGS</strong>
<button onClick={() => onSubmit?.current()}>Save</button>
</section>
);
}
export default function App() {
return (
<div className="App">
<SettingsContextProvider>
<ActionsBar />
<ExampleForm />
</SettingsContextProvider>
</div>
);
}
The main caveat I see in this approach is that you change the whole submit function when you need only reaction to submit event. Event is the catch, I think.
Your approach works ok, but has no extension points, for cases such as validation etc.
So I propose to use EventEmitter in any form (better with types support) as a context value e.g. communication channel.
This is a fork of your codesandbox that illustrates this approach:
https://codesandbox.io/s/friendly-fog-qlrusj?file=/src/App.tsx

How do you rerender a React component when an object's property is updated?

I have an object in my React application which is getting updated from another system. I'd like to display the properties of this object in a component, such that it updates in real time.
The object is tracked in my state manager, Jotai. I know that Jotai will only re-render my component when the actual object itself changes, not its properties. I'm not sure if that is possible.
Here is a sample that demonstrates my issue:
import React from "react";
import { Provider, atom, useAtom } from "jotai";
const myObject = { number: 0 };
const myObjectAtom = atom(myObject);
const myObjectPropertyAtom = atom((get) => {
const obj = get(myObjectAtom)
return obj.number
});
const ObjectDisplay = () => {
const [myObject] = useAtom(myObjectAtom);
const [myObjectProperty] = useAtom(myObjectPropertyAtom);
const forceUpdate = React.useState()[1].bind(null, {});
return (
<div>
{/* This doesn't update when the object updates */}
<p>{myObject.number}</p>
{/* This doesn't seem to work at all. */}
<p>{myObjectProperty}</p>
{/* I know you shouldn't do this, its just for demo */}
<button onClick={forceUpdate}>Force Update</button>
</div>
);
};
const App = () => {
// Update the object's property
setInterval(() => {
myObject.number += 0.1;
}, 100);
return (
<Provider>
<ObjectDisplay />
</Provider>
);
};
export default App;
Sandbox
you can use useEffect for this.
useEffect(()=> {
// code
}, [myObject.number])

Allow only one of the many modal dialog to be open at a time in React

I try to create a small bug tracking app with React 17. I have BugComponent where users can easily set priority, status, system, and some other properties by clicking on the related icon and select the value from a small popup dialog. It means I have 5-6 small modal dialogs for each bug and I have 20 bugs by default on the page.
With my current implementation, I have a component for displaying and changing priority like this:
export const PrioritySelector = ({priority}) => {
const [open, setOpen] = useState(false);
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
setOpen(!open);
}
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
setOpen(false);
}
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector open={open} onSelect={handleOnSelect} originalValue={prio}/>
</div>
);
}
I also have similar selectors for status and plannedEndDate.
The main BugComponent looks like this:
export const BugComponent = ({bug}) => {
return (
<div className="bug">
<StatusSelector status={bug.status} />
<div>{bug.text}</div>
<PrioritySelector priority={bug.priority} />
<DateSelector plannedEndDate={bug.plannedEndDate} />
<CommentIcon numberOfComments={bug.commentCount} />
</div>
)
}
It works, but the problem is the following:
If I open the dialog with clicking on the icon and then click on other icon of the same bug or even another bug without selecting a value then this modal dialog remains open. So it can happen that a lot of dialogs are open at a time.
Using vanilla JavaScript I would write something like this before open the new dialog:
document.querySelectorAll('.modal-selector.open').classList.remove('open');
How can I close dialogs before opening a new one in React?
You want to keep the information about what modal is currently open in a state variable above the BugComponent.
Do a state variable like this:
const NO_MODAL_EXPANDED = {bugId: "", componentType: ""};
const [expandedItem, setExpandedItem] = useState(NO_MODAL_EXPANDED);
// Then you pass these props down to the BugComponent:
<BugComponent
bug={bug}
hasExpandedItem={expandedItem.bugId === bug.bugId}
expandedItemType={expandedItem.componentType}
setExpandedItem={setExpandedItem}
closeModal={() => setExpandedItem(NoModalExpanded)}
/>;
In BugComponent you do:
export const BugComponent = ({
bug,
hasExpandedItem,
expandedItemType,
setExpandedItem,
closeModal
}) => {
const isExpanded = (name) => hasExpandedItem && expanedItemType === name;
return (
<div className="bug">
<StatusSelector
status={bug.status}
isExpanded={isExpanded("status")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "status"})}
/>
<div>{bug.text}</div>
<PrioritySelector
priority={bug.priority}
isExpanded={isExpanded("priority")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "priority"})}
/>
<DateSelector
plannedEndDate={bug.plannedEndDate}
isExpanded={isExpanded("date")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "date"})}
/>
<CommentIcon
numberOfComments={bug.commentCount}
isExpanded={isExpanded("comment")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "comment"})}
/>
</div>
);
};
And in the modal components you do:
export const PrioritySelector = ({
priority,
isExpanded,
expandItem,
closeItem,
}) => {
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
if (isExpanded) {
closeItem();
} else {
expandItem();
}
};
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
closeItem();
};
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector
open={isExpanded}
onSelect={handleOnSelect}
originalValue={priority}
/>
</div>
);
};
Read through it and see if you understand it. The core thing you need to understand is: you want to express the state in as few variables as possible. The only thing you want to know is: what component, if any, is expanded? Instead of distributing this state over a lot of components, you keep the state in the parent component that is the smallest common denominator.

react functional component with ag grid cannot call parent function via context

I am using ag-grid-react and ag-grid-community version 22.1.1. My app is written using functional components and hooks. I have a cellrenderer component that is attempting to call a handler within the parent component using the example found here. Is this a bug in ag-grid? I have been working on this application for over a year as I learn React, and this is my last major blocker so any help or a place to go to get that help would be greatly appreciated.
Cell Renderer Component
import React from 'react';
import Button from '../../button/button';
const RowButtonRenderer = props => {
const editClickHandler = (props) => {
let d = props.data;
console.log(d);
props.context.foo({d});
//props.editClicked(props);
}
const deleteClickHandler = (props) => {
props.deleteClicked(props);
}
return (<span>
<Button classname={'rowbuttons'} onClick={() => { editClickHandler(props) }} caption={'Edit'} />
<Button classname={'rowbuttons'} onClick={() => { deleteClickHandler(props) }} caption={'Delete'} />
</span>);
};
export default RowButtonRenderer;
Parent Component
function Checking() {
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: (props) => foo(props)};
const gridOptions = (params) => {
if (params.node.rowIndex % 2 === 0) {
return { background: "#ACC0C6" };
}
};
const frameworkComponents = {
rowButtonRenderer: RowButtonRenderer,
};
.
.
.
return (
<>
<AgGridReact
getRowStyle={gridOptions}
frameworkComponents={frameworkComponents}
context = {context}
columnDefs={columnDefinitions}
rowData={rowData}
headerHeight="50"
rowClass="gridFont"
></AgGridReact>
</>
);
}
When clicking the edit button on a row, the debugger says that there is a function.
This error is received though:
You are passing the context object in this code section:
const context = {componentParent: (props) => foo(props)};
...
<AgGridReact
context={context}
{...}
></AgGridReact>
And in your cell renderer you call this
props.context.foo({d});
While it should be this
props.context.componentParent({d});
Also you can assign your callback directly since it receives the same parameter and returns the same result (if any)
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: foo};
You can also use this shorthand syntax from ES6 when assigning object property
function componentParent(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent};
Live Demo

Tooltip delay on hover with RXJS

I'm trying to add tooltip delay (300msemphasized text) using rxjs (without setTimeout()). My goal is to have this logic inside of TooltipPopover component which will be later be reused and delay will be passed (if needed) as a prop.
I'm not sure how can I add "delay" logic inside of TooltipPopover component using rxjs?
Portal.js
const Portal = ({ children }) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("div");
useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);
return createPortal(children, el);
};
export default Portal;
TooltipPopover.js
import React from "react";
const TooltipPopover = ({ delay??? }) => {
return (
<div className="ant-popover-title">Title</div>
<div className="ant-popover-inner-content">{children}</div>
);
};
App.js
const App = () => {
return (
<Portal>
<TooltipPopover>
<div>
Content...
</div>
</TooltipPopover>
</Portal>
);
};
Then, I'm rendering TooltipPopover in different places:
ReactDOM.render(<TooltipPopover delay={1000}>
<SomeChildComponent/>
</TooltipPopover>, rootEl)
Here would be my approach:
mouseenter$.pipe(
// by default, the tooltip is not shown
startWith(CLOSE_TOOLTIP),
switchMap(
() => concat(timer(300), NEVER).pipe(
mapTo(SHOW_TOOLTIP),
takeUntil(mouseleave$),
endWith(CLOSE_TOOLTIP),
),
),
distinctUntilChanged(),
)
I'm not very familiar with best practices in React with RxJS, but this would be my reasoning. So, the flow would be this:
on mouseenter$, start the timer. concat(timer(300), NEVER) is used because although after 300ms the tooltip should be shown, we only want to hide it when mouseleave$ emits.
after 300ms, the tooltip is shown and will be closed mouseleave$
if mouseleave$ emits before 300ms pass, the CLOSE_TOOLTIP will emit, but you could avoid(I think) unnecessary re-renders with the help of distinctUntilChanged

Resources