From 2b0830c8bec3c639006289fc5752e2cf90f6bb3d Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 5 Feb 2019 12:39:25 -0800 Subject: [PATCH] Add light-test, and related changes (#173) * Add light-test, and related changes * Removed unnecessary call to destroyActors * Disabling directional light, for now. --- packages/functional-tests/src/app.ts | 16 +- .../src/tests/interpolation-test.ts | 16 +- .../functional-tests/src/tests/light-test.ts | 186 ++++++++++++++++++ .../src/tests/reparent-test.ts | 12 +- packages/sdk/src/adapters/multipeer/rules.ts | 78 -------- packages/sdk/src/math/quaternion.ts | 14 ++ packages/sdk/src/types/internal/context.ts | 107 +--------- .../src/types/network/payloads/payloads.ts | 23 --- .../sdk/src/types/network/payloads/physics.ts | 10 - packages/sdk/src/types/runtime/actor.ts | 90 +++++---- .../sdk/src/types/runtime/assets/material.ts | 18 +- packages/sdk/src/types/runtime/light.ts | 14 +- packages/sdk/src/utils/observe.ts | 17 +- 13 files changed, 309 insertions(+), 292 deletions(-) create mode 100644 packages/functional-tests/src/tests/light-test.ts diff --git a/packages/functional-tests/src/app.ts b/packages/functional-tests/src/app.ts index 9ccc64e62..426d67f2b 100644 --- a/packages/functional-tests/src/app.ts +++ b/packages/functional-tests/src/app.ts @@ -14,6 +14,7 @@ import GltfConcurrencyTest from './tests/gltf-concurrency-test'; import GltfGenTest from './tests/gltf-gen-test'; import InputTest from './tests/input-test'; import InterpolationTest from './tests/interpolation-test'; +import LightTest from './tests/light-test'; import LookAtTest from './tests/look-at-test'; import MutableAssetTest from './tests/mutable-asset-test'; import PrimitivesTest from './tests/primitives-test'; @@ -40,6 +41,7 @@ export default class App { /** * Registry of functional tests. Add your test here. + * *** KEEP LIST SORTED *** */ private testFactories: { [key: string]: (user: MRESDK.User) => Test } = { 'altspacevr-library-test': (): Test => new AltspaceVRLibraryTest(this, this.baseUrl), @@ -51,6 +53,7 @@ export default class App { 'gltf-gen-test': (): Test => new GltfGenTest(this, this.baseUrl), 'input-test': (): Test => new InputTest(this, this.baseUrl), 'interpolation-test': (): Test => new InterpolationTest(this), + 'light-test': (): Test => new LightTest(this, this.baseUrl), 'look-at-test': (user: MRESDK.User): Test => new LookAtTest(this, this.baseUrl, user), 'mutable-asset-test': (user: MRESDK.User): Test => new MutableAssetTest(this, this.baseUrl, user), 'primitives-test': (): Test => new PrimitivesTest(this, this.baseUrl), @@ -63,14 +66,11 @@ export default class App { constructor(private _context: MRESDK.Context, private params: MRESDK.ParameterSet, private baseUrl: string) { this._rpc = new MRERPC.ContextRPC(_context); - this.userJoined = this.userJoined.bind(this); - this.userLeft = this.userLeft.bind(this); - - this.context.onUserJoined(this.userJoined); - this.context.onUserLeft(this.userLeft); + this.context.onUserJoined((user) => this.userJoined(user)); + this.context.onUserLeft((user) => this.userLeft(user)); } - private userJoined = async (user: MRESDK.User) => { + private async userJoined(user: MRESDK.User) { console.log(`user-joined: ${user.name}, ${user.id}, ${user.properties.remoteAddress}`); this._connectedUsers[user.id] = user; @@ -90,10 +90,12 @@ export default class App { this.firstUser = user; } } - private userLeft = (user: MRESDK.User) => { + + private userLeft(user: MRESDK.User) { console.log(`user-left: ${user.name}, ${user.id}`); delete this._connectedUsers[user.id]; } + private async runTest(testName: string, user: MRESDK.User) { if (this.activeTests[testName]) { console.log(`Test already running: '${testName}'`); diff --git a/packages/functional-tests/src/tests/interpolation-test.ts b/packages/functional-tests/src/tests/interpolation-test.ts index cf63be5d5..a376ab661 100644 --- a/packages/functional-tests/src/tests/interpolation-test.ts +++ b/packages/functional-tests/src/tests/interpolation-test.ts @@ -5,7 +5,6 @@ import * as MRESDK from '@microsoft/mixed-reality-extension-sdk'; import App from '../app'; -import destroyActors from '../utils/destroyActors'; import Test from './test'; export default class InterpolationTest extends Test { @@ -22,26 +21,29 @@ export default class InterpolationTest extends Test { const timeout = setTimeout(() => this.running = false, 60000); await expressiveCubePromise; clearTimeout(timeout); - destroyActors(this.sceneRoot); return true; } private async spawnExpressiveCube() { - MRESDK.Actor.CreateEmpty(this.app.context, { + const label = MRESDK.Actor.CreateEmpty(this.app.context, { actor: { parentId: this.sceneRoot.id, transform: { position: { y: 2.5 } }, text: { - contents: 'Lerping scale and rotation\nClick to exit (or wait a minute)', + contents: + 'Lerping scale and rotation\n' + + 'Click to exit (or wait a minute)', anchor: MRESDK.TextAnchorLocation.TopCenter, justify: MRESDK.TextJustify.Center, height: 0.4, color: MRESDK.Color3.Yellow() } } - }); + }).value; + label.setBehavior(MRESDK.ButtonBehavior).onClick('released', () => this.running = false); + const cube = MRESDK.Actor.CreatePrimitive(this.app.context, { definition: { shape: MRESDK.PrimitiveShape.Box @@ -51,9 +53,7 @@ export default class InterpolationTest extends Test { parentId: this.sceneRoot.id, } }).value; - - const buttonBehavior = cube.setBehavior(MRESDK.ButtonBehavior); - buttonBehavior.onClick('released', () => this.running = false); + cube.setBehavior(MRESDK.ButtonBehavior).onClick('released', () => this.running = false); while (this.running) { // Random point on unit sphere (pick random axis). diff --git a/packages/functional-tests/src/tests/light-test.ts b/packages/functional-tests/src/tests/light-test.ts new file mode 100644 index 000000000..b52094029 --- /dev/null +++ b/packages/functional-tests/src/tests/light-test.ts @@ -0,0 +1,186 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as MRESDK from '@microsoft/mixed-reality-extension-sdk'; +import App from '../app'; +import Test from './test'; + +// tslint:disable:no-string-literal + +export default class LightTest extends Test { + private sceneRoot: MRESDK.Actor; + private running = true; + + constructor(app: App, private baseUrl: string) { + super(app); + } + + public async run() { + this.sceneRoot = MRESDK.Actor.CreateEmpty(this.app.context).value; + const runningTestPromise = this.runTest(); + const timeout = setTimeout(() => this.running = false, 60000); + await runningTestPromise; + clearTimeout(timeout); + return true; + } + + private async runTest() { + // Create scene objects. + const props = this.createProps(); + const label = this.createLabel(); + const sphere = this.createSphere(); + + // Click on anything to exit the test. + for (const actor of this.app.context.actors) { + actor.setBehavior(MRESDK.ButtonBehavior).onClick('released', () => this.running = false); + } + + // Updates the label for the test stage. + const updateLabel = (lightType: string) => { + label.text.contents = + `${lightType} Light Test\n` + + 'Click to exit (or wait a minute)'; + }; + + // Picks a random color. + const randomColor = (minValue = 0.15) => { + return new MRESDK.Color3( + minValue + Math.random() * (1 - minValue), + minValue + Math.random() * (1 - minValue), + minValue + Math.random() * (1 - minValue), + ); + }; + + // Animates the sphere along one side of the scene, with some randomness of final height, and + // rotating to face the center of the space. + const animateSide = async (dirX: number, dirZ: number, time: number) => { + if (this.running) { + const position = new MRESDK.Vector3(1.5 * dirX, 0.5 + Math.random() * 2, 1.5 * dirZ); + const rotation = MRESDK.Quaternion.LookAt( + position, + props['monkey'].transform.position, + new MRESDK.Vector3(0, -Math.PI / 8, 0)); + sphere.light.color = randomColor(); + await sphere.animateTo({ + transform: { + position, + rotation + } + }, time, MRESDK.AnimationEaseCurves.EaseInOutSine); + } + }; + + // One loop of the sphere moving along each side of the scene. + const animateAround = async (time: number) => { + await animateSide(1, 1, time / 4); + await animateSide(-1, 1, time / 4); + await animateSide(-1, -1, time / 4); + await animateSide(1, -1, time / 4); + }; + + while (this.running) { + // Spot Light + updateLabel('Spot'); + sphere.light.copy({ + type: 'spot', + spotAngle: Math.PI / 3, + intensity: 15, + range: 10, + }); + await animateAround(5); + // Point Light + updateLabel('Point'); + sphere.light.copy({ + type: 'point', + intensity: 10, + range: 10, + }); + await animateAround(5); + } + } + + private createProps() { + const props: { [id: string]: MRESDK.Actor } = {}; + props['monkey'] = MRESDK.Actor.CreateFromGltf(this.app.context, { + resourceUrl: `${this.baseUrl}/monkey.glb`, + colliderType: 'box', + actor: { + parentId: this.sceneRoot.id, + transform: { + scale: { x: 0.75, y: 0.75, z: 0.75 }, + position: { y: 1 }, + rotation: MRESDK.Quaternion.RotationAxis(MRESDK.Vector3.Up(), Math.PI) + } + } + }).value; + const propWidth = 0.4; + const propHeight = 0.4; + props['left-box'] = MRESDK.Actor.CreatePrimitive(this.app.context, { + definition: { + shape: MRESDK.PrimitiveShape.Box, + dimensions: { x: propWidth, z: propWidth, y: propHeight } + }, + addCollider: true, + actor: { + parentId: this.sceneRoot.id, + transform: { + position: { + x: -propWidth * 2, y: propHeight / 2 + } + } + } + }).value; + props['right-box'] = MRESDK.Actor.CreatePrimitive(this.app.context, { + definition: { + shape: MRESDK.PrimitiveShape.Box, + dimensions: { x: propWidth, z: propWidth, y: propHeight } + }, + addCollider: true, + actor: { + parentId: this.sceneRoot.id, + transform: { + position: { + x: propWidth * 2, y: propHeight / 2 + } + } + } + }).value; + return props; + } + + private createLabel() { + return MRESDK.Actor.CreateEmpty(this.app.context, { + actor: { + parentId: this.sceneRoot.id, + transform: { + position: { y: 3 } + }, + text: { + anchor: MRESDK.TextAnchorLocation.TopCenter, + justify: MRESDK.TextJustify.Center, + height: 0.4, + color: MRESDK.Color3.Yellow() + } + } + }).value; + } + + private createSphere() { + return MRESDK.Actor.CreatePrimitive(this.app.context, { + definition: { + shape: MRESDK.PrimitiveShape.Sphere, + radius: 0.1 + }, + addCollider: true, + actor: { + parentId: this.sceneRoot.id, + transform: { + position: { y: 1 } + }, + light: { type: 'spot' } // Add a light component. + } + }).value; + } +} diff --git a/packages/functional-tests/src/tests/reparent-test.ts b/packages/functional-tests/src/tests/reparent-test.ts index 5fe7a03e3..74850bdd6 100644 --- a/packages/functional-tests/src/tests/reparent-test.ts +++ b/packages/functional-tests/src/tests/reparent-test.ts @@ -6,7 +6,6 @@ import * as MRESDK from '@microsoft/mixed-reality-extension-sdk'; import App from '../app'; import delay from '../utils/delay'; -import destroyActors from '../utils/destroyActors'; import Test from './test'; export default class ReparentTest extends Test { @@ -23,12 +22,11 @@ export default class ReparentTest extends Test { const timeout = setTimeout(() => this.running = false, 60000); await runningTestPromise; clearTimeout(timeout); - destroyActors(this.sceneRoot); return true; } private async runTest() { - MRESDK.Actor.CreateEmpty(this.app.context, { + const label = MRESDK.Actor.CreateEmpty(this.app.context, { actor: { parentId: this.sceneRoot.id, transform: { @@ -45,7 +43,9 @@ export default class ReparentTest extends Test { color: MRESDK.Color3.Yellow() } } - }); + }).value; + label.setBehavior(MRESDK.ButtonBehavior).onClick('released', () => this.running = false); + const leftParent = MRESDK.Actor.CreateEmpty(this.app.context, { actor: { parentId: this.sceneRoot.id, @@ -73,13 +73,11 @@ export default class ReparentTest extends Test { parentId: leftParent.id } }).value; + sphere.setBehavior(MRESDK.ButtonBehavior).onClick('released', () => this.running = false); let currParent = 0; const parentIds = [leftParent.id, rightParent.id]; - const buttonBehavior = sphere.setBehavior(MRESDK.ButtonBehavior); - buttonBehavior.onClick('released', () => this.running = false); - while (this.running) { for (let i = 0; i < 10 && this.running; ++i) { await delay(100); diff --git a/packages/sdk/src/adapters/multipeer/rules.ts b/packages/sdk/src/adapters/multipeer/rules.ts index 4f51bcabf..829a29f4e 100644 --- a/packages/sdk/src/adapters/multipeer/rules.ts +++ b/packages/sdk/src/adapters/multipeer/rules.ts @@ -576,84 +576,6 @@ export const Rules: { [id in Payloads.PayloadType]: Rule } = { } }, - // ======================================================================== - 'enable-light': { - ...DefaultRule, - synchronization: { - stage: 'create-actors', - before: 'ignore', - during: 'queue', - after: 'allow' - }, - session: { - ...DefaultRule.session, - beforeReceiveFromApp: ( - session: Session, - message: Message - ) => { - const syncActor = session.actorSet[message.payload.actorId]; - if (syncActor) { - // Merge the new component into the existing actor. - syncActor.created.message.payload.actor.light = - deepmerge(syncActor.created.message.payload.actor.light || {}, message.payload.light); - } - return message; - } - } - }, - - // ======================================================================== - 'enable-rigidbody': { - ...DefaultRule, - synchronization: { - stage: 'create-actors', - before: 'ignore', - during: 'queue', - after: 'allow' - }, - session: { - ...DefaultRule.session, - beforeReceiveFromApp: ( - session: Session, - message: Message - ) => { - const syncActor = session.actorSet[message.payload.actorId]; - if (syncActor) { - // Merge the new component into the existing actor. - syncActor.created.message.payload.actor.rigidBody = - deepmerge(syncActor.created.message.payload.actor.rigidBody || {}, message.payload.rigidBody); - } - return message; - } - } - }, - - // ======================================================================== - 'enable-text': { - ...DefaultRule, - synchronization: { - stage: 'create-actors', - before: 'ignore', - during: 'queue', - after: 'allow' - }, - session: { - ...DefaultRule.session, - beforeReceiveFromApp: ( - session: Session, - message: Message - ) => { - const syncActor = session.actorSet[message.payload.actorId]; - if (syncActor) { - // Merge the new component into the existing actor. - syncActor.created.message.payload.actor.text = - deepmerge(syncActor.created.message.payload.actor.text || {}, message.payload.text); - } - return message; - } - } - }, - // ======================================================================== 'engine2app-rpc': { ...ClientOnlyRule diff --git a/packages/sdk/src/math/quaternion.ts b/packages/sdk/src/math/quaternion.ts index 130fb62c7..8e7d45ebf 100644 --- a/packages/sdk/src/math/quaternion.ts +++ b/packages/sdk/src/math/quaternion.ts @@ -462,6 +462,20 @@ export class Quaternion implements QuaternionLike { } } + /** + * Calculates a rotation to face the `to` point from the `from` point. + * @param from The location of the viewpoint. + * @param to The location to face toward. + * @param offset (Optional) Offset yaw, pitch, roll to add. + */ + public static LookAt(from: Vector3, to: Vector3, offset = Vector3.Zero()) { + const direction = to.subtract(from); + const yaw = -Math.atan2(direction.z, direction.x) + Math.PI / 2; + const len = Math.sqrt(direction.x * direction.x + direction.z * direction.z); + const pitch = -Math.atan2(direction.y, len); + return Quaternion.RotationYawPitchRoll(yaw + offset.x, pitch + offset.y, + offset.z); + } + /** * Returns the dot product (float) between the quaternions "left" and "right" * @param left defines the left operand diff --git a/packages/sdk/src/types/internal/context.ts b/packages/sdk/src/types/internal/context.ts index 303998438..08e5fa192 100644 --- a/packages/sdk/src/types/internal/context.ts +++ b/packages/sdk/src/types/internal/context.ts @@ -21,18 +21,12 @@ import { CollisionLayer, Context, CreateAnimationOptions, - Light, - LightLike, LookAtMode, PrimitiveDefinition, - RigidBody, - RigidBodyLike, SetAnimationStateOptions, SphereColliderParams, SubscriptionOwnerType, SubscriptionType, - Text, - TextLike, User, UserLike, UserSet, @@ -53,9 +47,6 @@ import { CreatePrimitive, DestroyActors, EnableCollider, - EnableLight, - EnableRigidBody, - EnableText, InterpolateActor, LookAt, ObjectSpawned, @@ -360,37 +351,7 @@ export class InternalContext { } } - public enableRigidBody(actorId: string, rigidBody?: Partial): ForwardPromise { - const actor = this.actorSet[actorId]; - if (!actor) { - return Promise.reject(`Failed enable rigid body. Actor ${actorId} not found`); - } else { - // Resolve by-reference values now, ensuring they won't change in the - // time between now and when this message is actually sent. - rigidBody = resolveJsonValues(rigidBody); - return createForwardPromise(actor.rigidBody, new Promise((resolve, reject) => { - actor.created().then(() => { - this.protocol.sendPayload({ - type: 'enable-rigidbody', - actorId, - rigidBody - } as EnableRigidBody, { - resolve: (payload: OperationResult) => { - this.protocol.recvPayload(payload); - if (payload.resultCode === 'error') { - reject(payload.message); - } else { - resolve(actor.rigidBody); - } - }, - reject - }); - }).catch((reason: any) => { - reject(`Failed enable rigid body on actor ${actor.id}. ${(reason || '').toString()}`.trim()); - }); - })); - } - } + /* TODO: Delete enable-collider payload and implement via patching mechanism. public enableCollider( actorId: string, @@ -469,71 +430,7 @@ export class InternalContext { })); } } - - public enableLight(actorId: string, light?: Partial): ForwardPromise { - const actor = this.actorSet[actorId]; - if (!actor) { - return Promise.reject(`Failed to enable light. Actor ${actorId} not found`); - } else { - // Resolve by-reference values now, ensuring they won't change in the - // time between now and when this message is actually sent. - light = resolveJsonValues(light); - return createForwardPromise(actor.light, new Promise((resolve, reject) => { - actor.created().then(() => { - this.protocol.sendPayload({ - type: 'enable-light', - actorId, - light, - } as EnableLight, { - resolve: (payload: OperationResult) => { - this.protocol.recvPayload(payload); - if (payload.resultCode === 'error') { - reject(payload.message); - } else { - resolve(actor.light); - } - }, - reject - }); - }).catch((reason: any) => { - reject(`Failed to enable light on ${actor.id}. ${(reason || '').toString()}`.trim()); - }); - })); - } - } - - public enableText(actorId: string, text?: Partial): ForwardPromise { - const actor = this.actorSet[actorId]; - if (!actor) { - return Promise.reject(`Failed to enable text. Actor ${actorId} not found`); - } else { - // Resolve by-reference values now, ensuring they won't change in the - // time between now and when this message is actually sent. - text = resolveJsonValues(text); - return createForwardPromise(actor.text, new Promise((resolve, reject) => { - actor.created().then(() => { - this.protocol.sendPayload({ - type: 'enable-text', - actorId, - text - } as EnableText, { - resolve: (payload: OperationResult) => { - this.protocol.recvPayload(payload); - if (payload.resultCode === 'error') { - reject(payload.message); - } else { - resolve(actor.text); - } - }, - reject - }); - }).catch((reason: any) => { - reject(`Failed to enable text on ${actor.id}. ${(reason || '').toString()}`.trim()); - }); - })); - } - } - + */ public updateSubscriptions( actorId: string, ownerType: SubscriptionOwnerType, diff --git a/packages/sdk/src/types/network/payloads/payloads.ts b/packages/sdk/src/types/network/payloads/payloads.ts index cf1388def..02ffd21bc 100644 --- a/packages/sdk/src/types/network/payloads/payloads.ts +++ b/packages/sdk/src/types/network/payloads/payloads.ts @@ -29,9 +29,6 @@ export type PayloadType | 'create-primitive' | 'destroy-actors' | 'enable-collider' - | 'enable-light' - | 'enable-rigidbody' - | 'enable-text' | 'engine2app-rpc' | 'handshake' | 'handshake-complete' @@ -319,26 +316,6 @@ export type SetBehavior = Payload & { behaviorType: BehaviorType; }; -/** - * @hidden - * App to engine. Add a light to the actor. - */ -export type EnableLight = Payload & { - type: 'enable-light'; - actorId: string; - light: Partial; -}; - -/** - * @hidden - * App to engine. Enable text on this actor. - */ -export type EnableText = Payload & { - type: 'enable-text'; - actorId: string; - text: Partial; -}; - /** * @hidden * Update subscription flags on the actor diff --git a/packages/sdk/src/types/network/payloads/physics.ts b/packages/sdk/src/types/network/payloads/physics.ts index 2f833cbb8..d446b860b 100644 --- a/packages/sdk/src/types/network/payloads/physics.ts +++ b/packages/sdk/src/types/network/payloads/physics.ts @@ -17,16 +17,6 @@ export enum CollisionEventType { TriggerExit = 'trigger-exit' } -/** - * @hidden - * App to engine. Enable the rigidbody on the actor. - */ -export type EnableRigidBody = Payload & { - type: 'enable-rigidbody'; - actorId: string; - rigidBody: Partial; -}; - /** * @hidden * App to engine. Send a rigidbody command diff --git a/packages/sdk/src/types/runtime/actor.ts b/packages/sdk/src/types/runtime/actor.ts index 8b67a3430..a64321184 100644 --- a/packages/sdk/src/types/runtime/actor.ts +++ b/packages/sdk/src/types/runtime/actor.ts @@ -138,7 +138,11 @@ export class Actor implements ActorLike, Patchable { this._emitter = new events.EventEmitter(); this._transform = new Transform(); // Actor patching: Observe the transform for changed values. - observe(this._transform, 'transform', (...path: string[]) => this.actorChanged(...path)); + observe({ + target: this._transform, + targetName: 'transform', + notifyChanged: (...path: string[]) => this.actorChanged(...path) + }); } /** @@ -266,30 +270,45 @@ export class Actor implements ActorLike, Patchable { * Adds a light component to the actor. * @param light Light characteristics. */ - public enableLight(light?: Partial): ForwardPromise { - if (!this._light) { + public enableLight(light?: Partial) { + if (!light && this._light) { + this.light.enabled = false; + } else if (!this._light) { this._light = new Light(); - this._light.copy(light); // Actor patching: Observe the light component for changed values. - observe(this._light, 'light', (...path: string[]) => this.actorChanged(...path)); - return this.context.internal.enableLight(this.id, light); + observe({ + target: this._light, + targetName: 'light', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); } - return createForwardPromise(this._light, Promise.resolve(this._light)); + // Copying the new values will trigger an actor update and enable/update the light component. + this._light.copy(light); } /** * Adds a rigid body component to the actor. * @param rigidBody Rigid body characteristics. */ - public enableRigidBody(rigidBody?: Partial): ForwardPromise { - if (!this._rigidBody) { + public enableRigidBody(rigidBody?: Partial) { + if (!rigidBody && this._rigidBody) { + // TODO: Add `enabled` field + // this.rigidBody.enabled = false; + } else if (!this._rigidBody) { this._rigidBody = new RigidBody(this); - this._rigidBody.copy(rigidBody); - // Actor patching: Observe the rigidBody component for changed values. - observe(this._rigidBody, 'rigidBody', (...path: string[]) => this.actorChanged(...path)); - return this.context.internal.enableRigidBody(this.id, rigidBody); + // Actor patching: Observe the rigid body component for changed values. + observe({ + target: this._rigidBody, + targetName: 'rigidBody', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); } - return createForwardPromise(this._rigidBody, Promise.resolve(this._rigidBody)); + // Copying the new values will trigger an actor update and enable/update the rigid body component. + this._rigidBody.copy(rigidBody); } // TODO @tombu: This will be enabled once the feature is ready for prime time. @@ -350,15 +369,22 @@ export class Actor implements ActorLike, Patchable { * Adds a text component to the actor. * @param text Text characteristics */ - public enableText(text?: Partial): ForwardPromise { - if (!this._text) { + public enableText(text?: Partial) { + if (!text && this._text) { + this.text.enabled = false; + } else if (!this._text) { this._text = new Text(); - this._text.copy(text); // Actor patching: Observe the text component for changed values. - observe(this._text, 'text', (...path: string[]) => this.actorChanged(...path)); - return this.context.internal.enableText(this.id, text); + observe({ + target: this._text, + targetName: 'text', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); } - return createForwardPromise(this._text, Promise.resolve(this._text)); + // Copying the new values will trigger an actor update and enable/update the text component. + this._text.copy(text); } /** @@ -587,7 +613,6 @@ export class Actor implements ActorLike, Patchable { const wasObserving = this.internal.observing; this.internal.observing = false; - // tslint:disable:curly if (!from) return this; if (from.id) this._id = from.id; if (from.parentId) this._parentId = from.parentId; @@ -595,28 +620,13 @@ export class Actor implements ActorLike, Patchable { if (from.tag) this._tag = from.tag; if (from.transform) this._transform.copy(from.transform); if (from.materialId) this._materialId = from.materialId; - if (from.light) { - if (!this._light) - this.enableLight(from.light); - else - this.light.copy(from.light); - } - if (from.rigidBody) { - if (!this._rigidBody) - this.enableRigidBody(from.rigidBody); - else - this.rigidBody.copy(from.rigidBody); - } + if (from.light) this.enableLight(from.light); + if (from.rigidBody) this.enableRigidBody(from.rigidBody); // TODO @tombu: Add in colliders here once feature is turned on. - if (from.text) { - if (!this._text) - this.enableText(from.text); - else - this.text.copy(from.text); - } + if (from.text) this.enableText(from.text); + this.internal.observing = wasObserving; return this; - // tslint:enable:curly } public toJSON() { diff --git a/packages/sdk/src/types/runtime/assets/material.ts b/packages/sdk/src/types/runtime/assets/material.ts index b7c3e941c..fc3bc29ca 100644 --- a/packages/sdk/src/types/runtime/assets/material.ts +++ b/packages/sdk/src/types/runtime/assets/material.ts @@ -105,9 +105,21 @@ export class Material extends Asset implements MaterialLike, Patchable this.materialChanged(...path)); - observe(this._mainTextureOffset, 'mainTextureOffset', (...path: string[]) => this.materialChanged(...path)); - observe(this._mainTextureScale, 'mainTextureScale', (...path: string[]) => this.materialChanged(...path)); + observe({ + target: this._color, + targetName: 'color', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); + observe({ + target: this._mainTextureOffset, + targetName: 'mainTextureOffset', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); + observe({ + target: this._mainTextureScale, + targetName: 'mainTextureScale', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); } public copy(from: Partial): this { diff --git a/packages/sdk/src/types/runtime/light.ts b/packages/sdk/src/types/runtime/light.ts index 1a49d137d..e7a83479e 100644 --- a/packages/sdk/src/types/runtime/light.ts +++ b/packages/sdk/src/types/runtime/light.ts @@ -5,7 +5,7 @@ import { Color3, Color3Like } from '../..'; -export type LightType = 'spot' | 'point' | 'directional'; +export type LightType = 'spot' | 'point'; export interface LightLike { enabled: boolean; @@ -17,13 +17,13 @@ export interface LightLike { } export class Light implements LightLike { - public enabled: boolean; - public type: LightType; - public intensity: number; + public enabled = true; + public type: LightType = 'point'; + public intensity = 1; // spot- and point-only: - public range: number; + public range = 1; // spot-only: - public spotAngle: number; + public spotAngle = Math.PI / 4; // tslint:disable:variable-name private _color: Color3; @@ -44,7 +44,7 @@ export class Light implements LightLike { if (!from) return this; if (from.enabled !== undefined) this.enabled = from.enabled; if (from.type !== undefined) this.type = from.type; - if (from.color !== undefined) this.color = from.color; + if (from.color !== undefined) this.color.copy(from.color); if (from.range !== undefined) this.range = from.range; if (from.intensity !== undefined) this.intensity = from.intensity; if (from.spotAngle !== undefined) this.spotAngle = from.spotAngle; diff --git a/packages/sdk/src/utils/observe.ts b/packages/sdk/src/utils/observe.ts index 117eb2dc1..daf20d06a 100644 --- a/packages/sdk/src/utils/observe.ts +++ b/packages/sdk/src/utils/observe.ts @@ -8,11 +8,17 @@ * Installs "watchers" for leaf properties in the target object, and calls the supplied callback * when they change and passing the entire path to the leaf, e.g.: ["transform", "position", "z"] */ -export default function observe(target: any, targetName: string, notifyChanged: (...path: string[]) => void) { - observeLeafProperties(target, [targetName], notifyChanged); +export default function observe(options: { + target: any, + targetName: string, + notifyChanged: (...path: string[]) => void, + triggerNotificationsNow?: boolean +}) { + observeLeafProperties(options.target, [options.targetName], options.notifyChanged, options.triggerNotificationsNow); } -function observeLeafProperties(target: any, path: string[], notifyChanged: (...path: string[]) => void) { +function observeLeafProperties( + target: any, path: string[], notifyChanged: (...path: string[]) => void, triggerNotificationsNow: boolean) { const names = Object.getOwnPropertyNames(target); for (const name of names) { // Fields starting with a dollar sign are not observed. @@ -49,8 +55,11 @@ function observeLeafProperties(target: any, path: string[], notifyChanged: (...p } }, }); + if (triggerNotificationsNow) { + notifyChanged(...path, publicName); + } } else if (type === 'object') { - observeLeafProperties(target[name], [...path, publicName], notifyChanged); + observeLeafProperties(target[name], [...path, publicName], notifyChanged, triggerNotificationsNow); } } }