0
votes

I am using react and three.js to display some 3D models in the browser. Everything works fine as long as the three.js component is the only element with visible content on the page. The three.js canvas will resize bigger and smaller just fine. You can wrap it in other containers and it still works fine. The issue starts when another element or component is added as a sibling.

I want to have a viewport with a fixed width sidebar. I'm doing this with a flexbox row container, wrapped around a sidebar component (a simple div with min-width set), and the responsive three.js component.

Resizing the window bigger works fine, the canvas fills the browser window appropriately. However, when resizing the window smaller, the canvas does not properly re-calculate it's size in relation to the available space left over by the static width sidebar, resulting in a canvas that does not fully fit in the browser and introducing scrollbars.

The canvas does shrink some, but not enough to keep it fully in the browser window. The same issue occurs vertically if a component sits above or below the three.js component. If you recompile the app while the browser window is smaller, the refreshed view will have the canvas properly resized.

Following the advice of many of the answers here on StackOverflow, the three.js react code looks something like this:

export class Viewport extends React.Component {

  componentDidMount() {

    const width = this.mount.clientWidth;
    const height = this.mount.clientHeight;
    window.addEventListener("resize", this.handleWindowResize);

    // setup scene
    this.scene = new THREE.Scene();

    //setup camera
    this.camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 1000 );
    this.camera.position.set( 0, 5, 10 );

    // setup rendering
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setClearColor('#666666');
    this.renderer.setSize(width, height, false);
    this.mount.appendChild(this.renderer.domElement);

    // setup geo
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: '#433F81' });
    this.cube = new THREE.Mesh(geometry, material);
    this.scene.add(this.cube);
    ...

  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleWindowResize);
    this.mount.removeChild(this.renderer.domElement);
  }

  handleWindowResize = () => {
    const width = this.mount.clientWidth;
    const height = this.mount.clientHeight;

    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();

    this.renderer.setSize(width, height, false);
  };

  render() {
    return (
      <div className="viewport" style={{display: 'flex', width: "100%", height: "100%"}} ref={(mount) => { this.mount = mount }} />
    );
  }
}

The styling is simple css flexbox, with the elements set to full width and height (100%), with the sidebar having a set min-width.

.flex-row-container {
  display: flex;
  flex-flow: row;
  height: 100%;
  width: 100%;
}

.sidebar {
  display: block;
  min-width: 250px;
  background-color: var(--color-mid-gray);
  margin: 3px 4px 3px 4px;
  box-sizing: border-box;
  border: 1px solid var(--color-mid-gray);
  outline: 3px solid var(--color-mid-gray);
}

What am I missing?

2
Why would you want display:flex on the div containing one item, the canvas? Isn't this div already within a div with display:flex (we can't see from your code.). If that is the case, then I would expect the div containing the canvas to have the flexbox item css eg. flex-basis, flex-grow etc., not display:flex.2pha
The parent div is indeed display:flex. Without the display:flex on the viewport div, the three.js canvas doesn’t fill the container completely, on compile or full page refresh. Switching it to flex item css actually makes the re-sizing worse. It’s odd. I’m sure it’s something subtle, I just don’t know what yet.meatwad

2 Answers

1
votes

So after commenting, I think I got it working how you want.

Basically,
I removed setting display:flex on the viewport, as this is a child of an element (the flex-row-container div) with display:flex, it does not need it, but does need flex:1 1 auto to set how it will display within the flexbox (which I did in the css rather than the JSX).
The problem then became that the viewport div would expand, but not contract (because it contains the canvas), so adding overflow:hidden fixed that.
After making those changes, this.mount.clientWidth would get the desired result that could then be used to set the three canvas size.

See This Fiddle.
I added rendering and animation to the example so the result could be easily seen.

Hope this helps.

0
votes

You might be getting incorrect values by using element.clientWidth. Have you tried using element.getBoundingClientRect() instead?

const rect = this.mount.getBoundingClientRect();
const width = rect.width;
const height = rect.height;