How to properly implement Babylon NME in react?

Hi everyone,
I am trying to use the node material editor in a react.js project, where we want to implement real-time collaboration. On the way I had a lot of problems with implementing babylon nme and hoped for help :)… What I had the most success with so far:

npm i babylonjs @babylonjs/loaders @babylonjs/node-editor

and imported it into my react component:

import * as BABYLON from 'babylonjs';
import '@babylonjs/loaders';
import {NodeEditor} from '@babylonjs/node-editor';

after that, I pretty much copy/pasted the index.js code from the nme-playground https://nme.babylonjs.com/ in my component. There I had to replace

BABYLON.NodeEditor.Show

with

NodeEditor.Show

After that, my page shows the NME-editor with the default project in it, so far so good. But strange things happen after I add new nodes or change the mesh. New nodes are not movable while the default ones are, clicking the grey background doesn’t work anymore and changing the preview mesh is only possible after maximizing the preview window. I get no errors. I attached the complete code below. I would be really thankful for any help, there is not so much documentation for the node editor :frowning:
Thanks in advance,
Simon

import React from "react";
import * as BABYLON from 'babylonjs';
import '@babylonjs/loaders';
import {NodeEditor} from '@babylonjs/node-editor';
import '../styles/cta4xsb.css';
import { editorStyles } from "../styles/styles";

export default function Editor() {
  const classes = editorStyles();

  var snippetUrl = "https://snippet.babylonjs.com";
  var currentSnippetToken;
  var previousHash = "";
  var nodeMaterial;
  var customLoadObservable = new BABYLON.Observable();
  var editorDisplayed = false;


  var cleanHash = function () {
    var splits = decodeURIComponent(window.location.hash.substr(1)).split("#");

    if (splits.length > 2) {
      splits.splice(2, splits.length - 2);
    }

    window.location.hash = splits.join("#");
  }

  var checkHash = function () {
    if (window.location.hash) {
      if (previousHash != window.location.hash) {
        cleanHash();

        previousHash = window.location.hash;

        try {
          var xmlHttp = new XMLHttpRequest();
          xmlHttp.onreadystatechange = function () {
            if (xmlHttp.readyState == 4) {
              if (xmlHttp.status == 200) {
                var snippet = JSON.parse(JSON.parse(xmlHttp.responseText).jsonPayload);
                let serializationObject = JSON.parse(snippet.nodeMaterial);

                if (editorDisplayed) {
                  customLoadObservable.notifyObservers(serializationObject);
                } else {
                  nodeMaterial.loadFromSerialization(serializationObject);
                  try {
                    nodeMaterial.build(true);
                  } catch (err) {
                    // Swallow the error here
                  }
                  showEditor();
                }
              }
            }
          }

          var hash = window.location.hash.substr(1);
          currentSnippetToken = hash.split("#")[0];
          xmlHttp.open("GET", snippetUrl + "/" + hash.replace("#", "/"));
          xmlHttp.send();
        } catch (e) {

        }
      }
    }

    setTimeout(checkHash, 200);
  }

  var showEditor = function () {
    editorDisplayed = true;

    var hostElement = document.getElementById("host-element");
    NodeEditor.Show({
      nodeMaterial: nodeMaterial,
      hostElement: hostElement,
      customLoadObservable: customLoadObservable,
      customSave: {
          label: "Save as unique URL",
          action: (data) => {
              return new Promise((resolve, reject) => {
                  var xmlHttp = new XMLHttpRequest();
                  xmlHttp.onreadystatechange = function () {
                      if (xmlHttp.readyState == 4) {
                          if (xmlHttp.status == 200) {
                              var baseUrl = window.location.href.replace(window.location.hash, "").replace(window.location.search, "");
                              var snippet = JSON.parse(xmlHttp.responseText);
                              var newUrl = baseUrl + "#" + snippet.id;
                              currentSnippetToken = snippet.id;
                              if (snippet.version && snippet.version != "0") {
                                  newUrl += "#" + snippet.version;
                              }
                              window.location.href = newUrl;
                              resolve();
                          }
                          else {
                              reject(`Unable to save your node material. It may be too large (${(dataToSend.payload.length / 1024).toFixed(2)} KB) because of embedded textures. Please reduce texture sizes or point to a specific url instead of embedding them and try again.`);
                          }
                      }
                  }

                  xmlHttp.open("POST", snippetUrl + (currentSnippetToken ? "/" + currentSnippetToken : ""), true);
                  xmlHttp.setRequestHeader("Content-Type", "application/json");

                  var dataToSend = {
                      payload : JSON.stringify({
                          nodeMaterial: data
                      }),
                      name: "",
                      description: "",
                      tags: ""
                  };
                  xmlHttp.send(JSON.stringify(dataToSend));
              });
          }
      }
    });
  }

  // Let's start
  if (BABYLON.Engine.isSupported()) {
    var canvas = document.createElement("canvas");
    var engine = new BABYLON.Engine(canvas, false, { disableWebGL2Support: false });
    var scene = new BABYLON.Scene(engine);
    var light0 = new BABYLON.HemisphericLight("light #0", new BABYLON.Vector3(0, 1, 0), scene);
    var light1 = new BABYLON.HemisphericLight("light #1", new BABYLON.Vector3(0, 1, 0), scene);
    var light2 = new BABYLON.HemisphericLight("light #2", new BABYLON.Vector3(0, 1, 0), scene);

    nodeMaterial = new BABYLON.NodeMaterial("node");

    // Set to default
    if (!window.location.hash) {
      const mode = BABYLON.DataStorage.ReadNumber("Mode", BABYLON.NodeMaterialModes.Material);

      switch (mode) {
        case BABYLON.NodeMaterialModes.Material:
          nodeMaterial.setToDefault();
          break;
        case BABYLON.NodeMaterialModes.PostProcess:
          nodeMaterial.setToDefaultPostProcess();
          break;
        case BABYLON.NodeMaterialModes.Particle:
          nodeMaterial.setToDefaultParticle();
          break;
        case BABYLON.NodeMaterialModes.ProceduralTexture:
          nodeMaterial.setToDefaultProceduralTexture();
          break;
      }
      nodeMaterial.build(true);
      showEditor();
    }
  }
  else {
    alert('Babylon.js is not supported.')
  }
  checkHash();

  return (

<div className={classes.element} id="host-element"></div>

  );
}

@RaananW will be back tomorrow and might be able to help with the setup ?

@simml do you happen to have a repro on github ? just the bare minimal project ? it might help with the troubleshooting

@sebavan, I will make a simplistic project later on today and will share it here.

1 Like

Hi @sebavan @RaananW , I created the simplified version of my project/problem here: https://github.com/samimaa/BabylonReact

I found the issue it will get fixed in our next npm release (I ll try to do it in a couple hours)

1 Like

@RaananW, for reference Fix NME in es6 · sebavan/Babylon.js@50e64c4 · GitHub

1 Like

Did you already manage to put it up on NPM? I’m still not getting it to work :frowning:

It is in the version alpha 43 on npm it was working in your example :frowning:

But I had to fix your code :slight_smile: a bit:

first in package.json only have @babylonjs/… and not babylonjs:

{
  "dependencies": {
    "@babylonjs/core": "^5.0.0-alpha.43",
    "@babylonjs/loaders": "^5.0.0-alpha.43",
    "@babylonjs/node-editor": "^5.0.0-alpha.43",
    "@material-ui/core": "^4.12.3"
  }
}

or the versions can not be used together

and in editor.jsx replace import * as BABYLON from 'babylonjs'; by import * as MYNAME from '@babylonjs/core';

BABYLON could create conflict so you need to pick another name here.

Thanks a lot so far for those hints! BUT I’m still not getting it right… I made the suggested changes and still getting the same behaviour. I committed the changes to GitHub - samimaa/BabylonReact. Could you maybe have another look at it?

Your latest version works for me without any changes

You probably still have left over of your previous version in some node_modules.

You should delete your nodes_modules folders and install them again just to be fully sure :slight_smile:

I tried:

  • Deleting the node_modules folder and reinstalling the dependencies
  • Switching to different hardware
  • Switching to different OS
  • Making a new react project and only installing the 5.0-alpha packages

All that showed the behaviour of my initial problem:

New nodes are not movable while the default ones are, clicking the grey background doesn’t work anymore and changing the preview mesh is only possible after maximizing the preview window

If that does work for you, I’m a bit at the end of my abilities…

Oh I did not test this part, only the fact that it renders and passes the undefined error.

I will have a look ASAP.

1 Like

So indeed, this behavior can repro. @RaananW master of build and web will have a look :slight_smile:

That took way longer than I want to admit :slight_smile:

For some odd reason, react (or the local react dev environment, i still haven’t fully figured that out) is running the showEditor function twice. Which means you have two node editors running on the same host element. Which is a big no no :slight_smile:

Add this to your load function, right after you check the host element:

if(hostElement.children.length) {
      return;
    }

this will prevent re-rendering this. Of course there are better approaches (like setting a state with editor-rendered flag), but it’s up to you.

1 Like

re rendering is the default in react dev from CRA cause it helps identifying reentrance issue in functions like componentDidUpdate and such. hope that helps @RaananW

It then would mean we could not rerender the NME ?

this is functional code, so there is no componentDidUpdate , but it does execute the render function twice. Solution here would be to use a state instead of executing the showEditor every time render is called.

But this is not on the NME level. it should be handled correctly on the react application.