Skip to content
This repository has been archived by the owner on Jul 20, 2020. It is now read-only.

[core] Allow rendering into object3ds #139

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/lib/React3Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,12 +582,12 @@ class React3Renderer {
};
}

const rootImage = markup;
const childImage = markup;

const rootMarkup = {
threeObject: container,
parentMarkup: null,
childrenMarkup: [rootImage],
childrenMarkup: [childImage],
toJSON: () => '---MARKUP---',
};

Expand All @@ -597,14 +597,18 @@ class React3Renderer {
markup: rootMarkup,
});

rootImage.parentMarkup = rootMarkup;
childImage.parentMarkup = rootMarkup;

const descriptorForChild = this.threeElementDescriptors[rootImage.elementType];
descriptorForChild.setParent(rootImage.threeObject, rootMarkup.threeObject);
const descriptorForChild = this.threeElementDescriptors[childImage.elementType];

const threeObject = childImage.threeObject;
const parentObject = rootMarkup.threeObject;

descriptorForChild.setParent(threeObject, parentObject);

// all objects now added can be marked as added to scene now!

rootImage.threeObject.mountedIntoRoot();
descriptorForChild.mountedIntoRoot(threeObject, parentObject);

const firstChild = container.userData.markup.childrenMarkup[0];
React3ComponentTree.precacheMarkup(instance, firstChild);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/descriptors/Geometry/GeometryDescriptorBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class GeometryDescriptorBase extends THREEElementDescriptor {
setParent(geometry, parentObject3D) {
invariant(parentObject3D instanceof THREE.Mesh
|| parentObject3D instanceof THREE.Points
|| parentObject3D instanceof THREE.Line, 'Parent is not a mesh');
|| parentObject3D instanceof THREE.Line, 'You are trying to add a geometry to an object which is not a mesh');
invariant(parentObject3D.geometry === undefined, 'Parent already has a geometry');

super.setParent(geometry, parentObject3D);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/descriptors/Light/LightDescriptorBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class LightDescriptorBase extends Object3DDescriptor {

const elementType = threeObject.userData.react3internalComponent._elementType;

warning(this._warnedAboutLightMaterialUpdate,
warning(false,
LightDescriptorBase.getDynamicWarningMessage(elementType, owner));
this._warnedAboutLightMaterialUpdate = true;
}
Expand Down
23 changes: 23 additions & 0 deletions src/lib/descriptors/Object/Object3DDescriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,29 @@ class Object3DDescriptor extends THREEElementDescriptor {
hideHighlight(threeObject) {
threeObject.userData.events.emit('hideHighlight');
}

mountedIntoRoot(threeObject, parentObject) {
if (process.env.NODE_ENV !== 'production') {
invariant(parentObject instanceof THREE.Object3D,
'Can mount objects only into other objects');

invariant(parentObject.children.length === 0, 'Can mount objects only into other objects' +
' which have no other children');
}

parentObject.add(threeObject);

threeObject.userData._isRootObject = true;
}


unmount(threeObject) {
if (threeObject.userData._isRootObject) {
threeObject.parent.remove(threeObject);
}

return super.unmount(threeObject);
}
}

module.exports = Object3DDescriptor;
4 changes: 4 additions & 0 deletions src/lib/descriptors/React3Descriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ class React3Descriptor extends THREEElementDescriptor {

return super.componentWillUnmount(threeObject);
}

mountedIntoRoot(threeObject) {
threeObject.mountedIntoRoot();
}
}

module.exports = React3Descriptor;
5 changes: 5 additions & 0 deletions src/lib/descriptors/THREEElementDescriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ class THREEElementDescriptor {

}

mountedIntoRoot(threeObject, parentObject) { // eslint-disable-line no-unused-vars
warning(false, `This type (${this.constructor.name}) cannot be mounted as a root element.` +
' Please mount classes that derive from object3D or react3');
}

unmount(threeObject) {
const markup = threeObject.userData.markup;

Expand Down
144 changes: 143 additions & 1 deletion tests/src/core/React3/React3Mounts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom';
import chai from 'chai';
import sinon from 'sinon';
import * as THREE from 'three';

module.exports = (type) => {
const { testDiv, React3, mockConsole } = require('../../utils/initContainer')(type);
const { testDiv, React3, mockConsole, requireHelper } = require('../../utils/initContainer')(type);

const { expect } = chai;

Expand Down Expand Up @@ -130,4 +131,145 @@ module.exports = (type) => {
</React3>
</div>, testDiv);
});

describe('manual rendering', () => {
let canvas;
let React3Renderer;

const onRecreateCanvas = () => {
};

before(() => {
canvas = document.createElement('canvas');
React3Renderer = requireHelper('React3Renderer');

testDiv.appendChild(canvas);
});

describe('react3', () => {
let react3Renderer;
before(() => {
react3Renderer = new React3Renderer();
});

it('can be rendered into a canvas', () => {
const react3Ref = sinon.spy();

mockConsole.expectThreeLog();

react3Renderer.render(
(<react3
context="3d"
width={50}
height={50}
onRecreateCanvas={onRecreateCanvas}
ref={react3Ref}
/>), canvas);

expect(react3Ref.callCount).to.equal(1);
});

after(() => {
react3Renderer.dispose();
});
});

describe('objects', () => {
let react3Renderer;

beforeEach(() => {
react3Renderer = new React3Renderer();
});

it('can be rendered into other objects', () => {
const containerObject = new THREE.Object3D();

const object3dRef = sinon.spy();

react3Renderer.render((<object3D ref={object3dRef} />), containerObject);

expect(object3dRef.callCount).to.equal(1);

expect(object3dRef.lastCall.args[0], 'Object should be a child of the container')
.to.equal(containerObject.children[0]);
});

it('can be rendered into deeper objects', () => {
const containerObject = new THREE.Object3D();

const object3dRef = sinon.spy();
const secondObject3dRef = sinon.spy();

react3Renderer.render((<object3D ref={object3dRef}>
<object3D ref={secondObject3dRef} />
</object3D>), containerObject);

expect(object3dRef.callCount).to.equal(1);
expect(secondObject3dRef.callCount).to.equal(1);

expect(object3dRef.lastCall.args[0], 'Object should be a child of the container')
.to.equal(containerObject.children[0]);

expect(secondObject3dRef.lastCall.args[0], 'The second object should be a child of the first object')
.to.equal(object3dRef.lastCall.args[0].children[0]);
});

it('can be unmounted from other objects', () => {
const containerObject = new THREE.Object3D();

const object3dRef = sinon.spy();

react3Renderer.render((<object3D ref={object3dRef} />), containerObject);
react3Renderer.unmountComponentAtNode(containerObject);

expect(object3dRef.callCount).to.equal(2);
expect(object3dRef.lastCall.args[0], 'Object should be removed from the container')
.to.be.null();
expect(containerObject.children.length).to.equal(0);
});

it('cannot be unmounted from non-root objects', () => {
const containerObject = new THREE.Object3D();

const object3dRef = sinon.spy();
const secondObject3dRef = sinon.spy();

react3Renderer.render((<object3D ref={object3dRef}>
<object3D ref={secondObject3dRef} />
</object3D>), containerObject);

mockConsole.expectDev('Warning: unmountComponentAtNode(): The node you\'re' +
' attempting to unmount was rendered by React and is not a top-level container.' +
' You may have accidentally passed in a React root node instead of its container.');

react3Renderer.unmountComponentAtNode(object3dRef.lastCall.args[0]);

react3Renderer.unmountComponentAtNode(secondObject3dRef.lastCall.args[0]);
});

it('cannot be mounted if it does not extend object3d', () => {
const containerObject = new THREE.Mesh();

containerObject.geometry.dispose();
containerObject.geometry = undefined;

const geometryRef = sinon.spy();
react3Renderer.render((<geometry
ref={geometryRef}
vertices={[]}
/>), containerObject);

mockConsole.expectDev('Warning: This type (GeometryDescriptor) cannot be mounted as a root element.' +
' Please mount classes that derive from object3D or react3');
});

afterEach(() => {
react3Renderer.dispose();
});
});

after(() => {
testDiv.removeChild(canvas);
});
});
};
28 changes: 15 additions & 13 deletions tests/src/utils/MockConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ module.exports = class MockConsole {

if (_messages.length > 0) {
assert(false, `Messages received but not expected:
${_messages.map(({ args, stack }, i) => `${i}: ${this._printArgs(args, stack)}`).join('\n')}`);
${_messages.map(({ args, error }, i) => `${i}: ${this._printArgs(args, error)}`).join('\n')}`);
} else if (_expectedMessages.length > 0) {
assert(false, `Messages expected but not received:
${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n')}`);
Expand Down Expand Up @@ -108,7 +108,7 @@ ${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n'

args.type = 'LOG';

this._messageReceived(args, new Error().stack);
this._messageReceived(args, new Error());
}

/**
Expand All @@ -122,7 +122,7 @@ ${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n'

args.type = 'WARNING';

this._messageReceived(args, new Error().stack);
this._messageReceived(args, new Error());
}

/**
Expand All @@ -136,47 +136,47 @@ ${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n'

args.type = 'ERROR';

this._messageReceived(args, new Error().stack);
this._messageReceived(args, new Error());
}

/* private methods */

/**
*
* @param args {Array.<*>}
* @param [stack]
* @param [error]
* @returns {string}
* @private
*/
_printArgs(args, stack) {
_printArgs(args, error) {
return `[${args.type || 'LOG'}]|${args.map(arg =>
util.inspect(arg, {}))
.join('\t')}${stack ? `\n${stack}\n` : ''}`;
.join('\t')}${error ? `\n${error.stack}\n` : ''}`;
}

/**
*
* @param args {Array.<*>}
* @param [stack]
* @param [error]
* @returns {string}
* @private
*/
_messageReceived(args, stack) {
_messageReceived(args, error) {
const {
_messages,
_expectedMessages,
} = this;

if (_expectedMessages.length > 0) {
this._checkMessage(_expectedMessages.shift(), { args, stack });
this._checkMessage(_expectedMessages.shift(), { args, error });

if (_expectedMessages.length === 0) {
this._events.emit('empty');
}
} else {
_messages.push({
args,
stack,
error,
});
}
}
Expand All @@ -185,10 +185,10 @@ ${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n'
*
* @param expectedArgs {Array.<*>}
* @param actualArgs
* @param stack
* @param error
* @private
*/
_checkMessage(expectedArgs, { args: actualArgs, stack }) {
_checkMessage(expectedArgs, { args: actualArgs, error }) {
const expectedMessage = `${expectedArgs.join('\t')}`;
const actualMessage = `${actualArgs.join('\t')}`;

Expand All @@ -202,6 +202,8 @@ ${_expectedMessages.map((args, i) => `${i}: ${this._printArgs(args)}`).join('\n'
_messages.length = 0;
_expectedMessages.length = 0;

const stack = error.stack;

assert(false, `Log error, expected message:
> ${expectedMessage}
But received message:
Expand Down
Loading