Change indentation to tabs from spaces (#43)
This commit is contained in:
Родитель
ee8e57b2d1
Коммит
a828cffde2
|
@ -4,137 +4,137 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
Actor,
|
||||
AnimationEaseCurves,
|
||||
AnimationKeyframe,
|
||||
AnimationWrapMode,
|
||||
ButtonBehavior,
|
||||
Context,
|
||||
Quaternion,
|
||||
TextAnchorLocation,
|
||||
Vector3
|
||||
Actor,
|
||||
AnimationEaseCurves,
|
||||
AnimationKeyframe,
|
||||
AnimationWrapMode,
|
||||
ButtonBehavior,
|
||||
Context,
|
||||
Quaternion,
|
||||
TextAnchorLocation,
|
||||
Vector3
|
||||
} from '@microsoft/mixed-reality-extension-sdk';
|
||||
|
||||
/**
|
||||
* The main class of this app. All the logic goes here.
|
||||
*/
|
||||
export default class HelloWorld {
|
||||
private text: Actor = null;
|
||||
private cube: Actor = null;
|
||||
private text: Actor = null;
|
||||
private cube: Actor = null;
|
||||
|
||||
constructor(private context: Context, private baseUrl: string) {
|
||||
this.context.onStarted(() => this.started());
|
||||
}
|
||||
constructor(private context: Context, private baseUrl: string) {
|
||||
this.context.onStarted(() => this.started());
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the context is "started", initialize the app.
|
||||
*/
|
||||
private started() {
|
||||
// Create a new actor with no mesh, but some text.
|
||||
this.text = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: 'Text',
|
||||
transform: {
|
||||
app: { position: { x: 0, y: 0.5, z: 0 } }
|
||||
},
|
||||
text: {
|
||||
contents: "Hello World!",
|
||||
anchor: TextAnchorLocation.MiddleCenter,
|
||||
color: { r: 30 / 255, g: 206 / 255, b: 213 / 255 },
|
||||
height: 0.3
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Once the context is "started", initialize the app.
|
||||
*/
|
||||
private started() {
|
||||
// Create a new actor with no mesh, but some text.
|
||||
this.text = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: 'Text',
|
||||
transform: {
|
||||
app: { position: { x: 0, y: 0.5, z: 0 } }
|
||||
},
|
||||
text: {
|
||||
contents: "Hello World!",
|
||||
anchor: TextAnchorLocation.MiddleCenter,
|
||||
color: { r: 30 / 255, g: 206 / 255, b: 213 / 255 },
|
||||
height: 0.3
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Here we create an animation on our text actor. Animations have three mandatory arguments:
|
||||
// a name, an array of keyframes, and an array of events.
|
||||
this.text.createAnimation(
|
||||
// The name is a unique identifier for this animation. We'll pass it to "startAnimation" later.
|
||||
"Spin", {
|
||||
// Keyframes define the timeline for the animation: where the actor should be, and when.
|
||||
// We're calling the generateSpinKeyframes function to produce a simple 20-second revolution.
|
||||
keyframes: this.generateSpinKeyframes(20, Vector3.Up()),
|
||||
// Events are points of interest during the animation. The animating actor will emit a given
|
||||
// named event at the given timestamp with a given string value as an argument.
|
||||
events: [],
|
||||
// Here we create an animation on our text actor. Animations have three mandatory arguments:
|
||||
// a name, an array of keyframes, and an array of events.
|
||||
this.text.createAnimation(
|
||||
// The name is a unique identifier for this animation. We'll pass it to "startAnimation" later.
|
||||
"Spin", {
|
||||
// Keyframes define the timeline for the animation: where the actor should be, and when.
|
||||
// We're calling the generateSpinKeyframes function to produce a simple 20-second revolution.
|
||||
keyframes: this.generateSpinKeyframes(20, Vector3.Up()),
|
||||
// Events are points of interest during the animation. The animating actor will emit a given
|
||||
// named event at the given timestamp with a given string value as an argument.
|
||||
events: [],
|
||||
|
||||
// Optionally, we also repeat the animation infinitely. PingPong alternately runs the animation
|
||||
// foward then backward.
|
||||
wrapMode: AnimationWrapMode.PingPong
|
||||
});
|
||||
// Optionally, we also repeat the animation infinitely. PingPong alternately runs the animation
|
||||
// foward then backward.
|
||||
wrapMode: AnimationWrapMode.PingPong
|
||||
});
|
||||
|
||||
// Load a glTF model
|
||||
this.cube = Actor.CreateFromGLTF(this.context, {
|
||||
// at the given URL
|
||||
resourceUrl: `${this.baseUrl}/altspace-cube.glb`,
|
||||
// and spawn box colliders around the meshes.
|
||||
colliderType: 'box',
|
||||
// Also apply the following generic actor properties.
|
||||
actor: {
|
||||
name: 'Altspace Cube',
|
||||
// Parent the glTF model to the text actor.
|
||||
parentId: this.text.id,
|
||||
transform: {
|
||||
local: {
|
||||
position: { x: 0, y: -1, z: 0 },
|
||||
scale: { x: 0.4, y: 0.4, z: 0.4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Load a glTF model
|
||||
this.cube = Actor.CreateFromGLTF(this.context, {
|
||||
// at the given URL
|
||||
resourceUrl: `${this.baseUrl}/altspace-cube.glb`,
|
||||
// and spawn box colliders around the meshes.
|
||||
colliderType: 'box',
|
||||
// Also apply the following generic actor properties.
|
||||
actor: {
|
||||
name: 'Altspace Cube',
|
||||
// Parent the glTF model to the text actor.
|
||||
parentId: this.text.id,
|
||||
transform: {
|
||||
local: {
|
||||
position: { x: 0, y: -1, z: 0 },
|
||||
scale: { x: 0.4, y: 0.4, z: 0.4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create some animations on the cube.
|
||||
this.cube.createAnimation(
|
||||
'DoAFlip', {
|
||||
keyframes: this.generateSpinKeyframes(1.0, Vector3.Right()),
|
||||
events: []
|
||||
});
|
||||
// Create some animations on the cube.
|
||||
this.cube.createAnimation(
|
||||
'DoAFlip', {
|
||||
keyframes: this.generateSpinKeyframes(1.0, Vector3.Right()),
|
||||
events: []
|
||||
});
|
||||
|
||||
// Now that the text and its animation are all being set up, we can start playing
|
||||
// the animation.
|
||||
this.text.enableAnimation('Spin');
|
||||
// Now that the text and its animation are all being set up, we can start playing
|
||||
// the animation.
|
||||
this.text.enableAnimation('Spin');
|
||||
|
||||
// Set up cursor interaction. We add the input behavior ButtonBehavior to the cube.
|
||||
// Button behaviors have two pairs of events: hover start/stop, and click start/stop.
|
||||
const buttonBehavior = this.cube.setBehavior(ButtonBehavior);
|
||||
// Set up cursor interaction. We add the input behavior ButtonBehavior to the cube.
|
||||
// Button behaviors have two pairs of events: hover start/stop, and click start/stop.
|
||||
const buttonBehavior = this.cube.setBehavior(ButtonBehavior);
|
||||
|
||||
// Trigger the grow/shrink animations on hover.
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
this.cube.animateTo(
|
||||
{ transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }, 0.3, AnimationEaseCurves.EaseOutSine);
|
||||
});
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
this.cube.animateTo(
|
||||
{ transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }, 0.3, AnimationEaseCurves.EaseOutSine);
|
||||
});
|
||||
// Trigger the grow/shrink animations on hover.
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
this.cube.animateTo(
|
||||
{ transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }, 0.3, AnimationEaseCurves.EaseOutSine);
|
||||
});
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
this.cube.animateTo(
|
||||
{ transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }, 0.3, AnimationEaseCurves.EaseOutSine);
|
||||
});
|
||||
|
||||
// When clicked, do a 360 sideways.
|
||||
buttonBehavior.onClick(_ => {
|
||||
this.cube.enableAnimation('DoAFlip');
|
||||
});
|
||||
}
|
||||
// When clicked, do a 360 sideways.
|
||||
buttonBehavior.onClick(_ => {
|
||||
this.cube.enableAnimation('DoAFlip');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate keyframe data for a simple spin animation.
|
||||
* @param duration The length of time in seconds it takes to complete a full revolution.
|
||||
* @param axis The axis of rotation in local space.
|
||||
*/
|
||||
private generateSpinKeyframes(duration: number, axis: Vector3): AnimationKeyframe[] {
|
||||
return [{
|
||||
time: 0 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 0) } } }
|
||||
}, {
|
||||
time: 0.25 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 0.5 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI) } } }
|
||||
}, {
|
||||
time: 0.75 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 3 * Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 1 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 2 * Math.PI) } } }
|
||||
}];
|
||||
}
|
||||
/**
|
||||
* Generate keyframe data for a simple spin animation.
|
||||
* @param duration The length of time in seconds it takes to complete a full revolution.
|
||||
* @param axis The axis of rotation in local space.
|
||||
*/
|
||||
private generateSpinKeyframes(duration: number, axis: Vector3): AnimationKeyframe[] {
|
||||
return [{
|
||||
time: 0 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 0) } } }
|
||||
}, {
|
||||
time: 0.25 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 0.5 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI) } } }
|
||||
}, {
|
||||
time: 0.75 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 3 * Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 1 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 2 * Math.PI) } } }
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ dotenv.config();
|
|||
|
||||
// Start listening for connections, and serve static files
|
||||
const server = new WebHost({
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
});
|
||||
|
||||
// Handle new application sessions
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"forin": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
"tabs"
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
|
|
|
@ -9,31 +9,31 @@ import * as MRESDK from '@microsoft/mixed-reality-extension-sdk';
|
|||
* Solar system database
|
||||
*/
|
||||
interface Database {
|
||||
[key: string]: DatabaseRecord;
|
||||
[key: string]: DatabaseRecord;
|
||||
}
|
||||
|
||||
interface DatabaseRecord {
|
||||
name: string;
|
||||
parent: string;
|
||||
diameter: number; // km
|
||||
distance: number; // 10^6 km
|
||||
day: number; // hours
|
||||
year: number; // days
|
||||
inclination: number; // degrees
|
||||
obliquity: number; // degrees
|
||||
retrograde: boolean;
|
||||
name: string;
|
||||
parent: string;
|
||||
diameter: number; // km
|
||||
distance: number; // 10^6 km
|
||||
day: number; // hours
|
||||
year: number; // days
|
||||
inclination: number; // degrees
|
||||
obliquity: number; // degrees
|
||||
retrograde: boolean;
|
||||
}
|
||||
|
||||
interface CelestialBody {
|
||||
inclination: MRESDK.Actor;
|
||||
position: MRESDK.Actor;
|
||||
obliquity0: MRESDK.Actor;
|
||||
obliquity1: MRESDK.Actor;
|
||||
model: MRESDK.Actor;
|
||||
inclination: MRESDK.Actor;
|
||||
position: MRESDK.Actor;
|
||||
obliquity0: MRESDK.Actor;
|
||||
obliquity1: MRESDK.Actor;
|
||||
model: MRESDK.Actor;
|
||||
}
|
||||
|
||||
interface CelestialBodySet {
|
||||
[key: string]: CelestialBody;
|
||||
[key: string]: CelestialBody;
|
||||
}
|
||||
|
||||
// Data source: https://nssdc.gsfc.nasa.gov/planetary/dataheet/
|
||||
|
@ -45,305 +45,305 @@ const database: Database = require('../public/database.json');
|
|||
* Solar System Application
|
||||
*/
|
||||
export default class SolarSystem {
|
||||
private celestialBodies: CelestialBodySet = {};
|
||||
private animationsRunning = false;
|
||||
private celestialBodies: CelestialBodySet = {};
|
||||
private animationsRunning = false;
|
||||
|
||||
constructor(private context: MRESDK.Context, private baseUrl: string) {
|
||||
this.context.onUserJoined(user => this.userJoined(user));
|
||||
this.context.onUserLeft(user => this.userLeft(user));
|
||||
this.context.onStarted(() => this.started());
|
||||
this.context.onStopped(() => this.stopped());
|
||||
}
|
||||
constructor(private context: MRESDK.Context, private baseUrl: string) {
|
||||
this.context.onUserJoined(user => this.userJoined(user));
|
||||
this.context.onUserLeft(user => this.userLeft(user));
|
||||
this.context.onStarted(() => this.started());
|
||||
this.context.onStopped(() => this.stopped());
|
||||
}
|
||||
|
||||
private started = () => {
|
||||
console.log(`session started ${this.context.sessionId}`);
|
||||
private started = () => {
|
||||
console.log(`session started ${this.context.sessionId}`);
|
||||
|
||||
this.createSolarSystem();
|
||||
this.createSolarSystem();
|
||||
|
||||
const sunEntity = this.celestialBodies.sol;
|
||||
if (sunEntity && sunEntity.model) {
|
||||
const sun = sunEntity.model;
|
||||
const sunPrimitives = sun.findChildrenByName('Primitive', true);
|
||||
const sunEntity = this.celestialBodies.sol;
|
||||
if (sunEntity && sunEntity.model) {
|
||||
const sun = sunEntity.model;
|
||||
const sunPrimitives = sun.findChildrenByName('Primitive', true);
|
||||
|
||||
sunPrimitives.forEach((prim) => {
|
||||
// Add a collider so that the behavior system will work properly on Unity host apps.
|
||||
const center = { x: 0, y: 0, z: 0 } as MRESDK.Vector3Like;
|
||||
const radius = 3;
|
||||
prim.setCollider('sphere', false, center, radius);
|
||||
sunPrimitives.forEach((prim) => {
|
||||
// Add a collider so that the behavior system will work properly on Unity host apps.
|
||||
const center = { x: 0, y: 0, z: 0 } as MRESDK.Vector3Like;
|
||||
const radius = 3;
|
||||
prim.setCollider('sphere', false, center, radius);
|
||||
|
||||
const buttonBehavior = prim.setBehavior(MRESDK.ButtonBehavior);
|
||||
const buttonBehavior = prim.setBehavior(MRESDK.ButtonBehavior);
|
||||
|
||||
buttonBehavior.onClick(_ => {
|
||||
if (this.animationsRunning) {
|
||||
this.pauseAnimations();
|
||||
this.animationsRunning = false;
|
||||
} else {
|
||||
this.resumeAnimations();
|
||||
this.animationsRunning = true;
|
||||
}
|
||||
});
|
||||
buttonBehavior.onClick(_ => {
|
||||
if (this.animationsRunning) {
|
||||
this.pauseAnimations();
|
||||
this.animationsRunning = false;
|
||||
} else {
|
||||
this.resumeAnimations();
|
||||
this.animationsRunning = true;
|
||||
}
|
||||
});
|
||||
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
console.log(`Hover entered on ${sunEntity.model.name}.`);
|
||||
});
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
console.log(`Hover entered on ${sunEntity.model.name}.`);
|
||||
});
|
||||
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
console.log(`Hover exited on ${sunEntity.model.name}.`);
|
||||
});
|
||||
});
|
||||
}
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
console.log(`Hover exited on ${sunEntity.model.name}.`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.resumeAnimations();
|
||||
this.animationsRunning = true;
|
||||
}
|
||||
this.resumeAnimations();
|
||||
this.animationsRunning = true;
|
||||
}
|
||||
|
||||
private stopped() {
|
||||
console.log(`session stopped ${this.context.sessionId}`);
|
||||
}
|
||||
private stopped() {
|
||||
console.log(`session stopped ${this.context.sessionId}`);
|
||||
}
|
||||
|
||||
private userJoined(user: MRESDK.User) {
|
||||
console.log(`user-joined: ${user.name}, ${user.id}`);
|
||||
}
|
||||
private userJoined(user: MRESDK.User) {
|
||||
console.log(`user-joined: ${user.name}, ${user.id}`);
|
||||
}
|
||||
|
||||
private userLeft(user: MRESDK.User) {
|
||||
console.log(`user-left: ${user.name}`);
|
||||
}
|
||||
private userLeft(user: MRESDK.User) {
|
||||
console.log(`user-left: ${user.name}`);
|
||||
}
|
||||
|
||||
private createSolarSystem() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
this.createBody(bodyName);
|
||||
}
|
||||
}
|
||||
private createSolarSystem() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
this.createBody(bodyName);
|
||||
}
|
||||
}
|
||||
|
||||
private resumeAnimations() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
celestialBody.model.resumeAnimation(`${bodyName}:axial`);
|
||||
celestialBody.position.resumeAnimation(`${bodyName}:orbital`);
|
||||
}
|
||||
}
|
||||
private resumeAnimations() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
celestialBody.model.resumeAnimation(`${bodyName}:axial`);
|
||||
celestialBody.position.resumeAnimation(`${bodyName}:orbital`);
|
||||
}
|
||||
}
|
||||
|
||||
private pauseAnimations() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
celestialBody.model.pauseAnimation(`${bodyName}:axial`);
|
||||
celestialBody.position.pauseAnimation(`${bodyName}:orbital`);
|
||||
}
|
||||
}
|
||||
private pauseAnimations() {
|
||||
const keys = Object.keys(database);
|
||||
for (const bodyName of keys) {
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
celestialBody.model.pauseAnimation(`${bodyName}:axial`);
|
||||
celestialBody.position.pauseAnimation(`${bodyName}:orbital`);
|
||||
}
|
||||
}
|
||||
|
||||
private createBody(bodyName: string) {
|
||||
console.log(`Loading ${bodyName}`);
|
||||
private createBody(bodyName: string) {
|
||||
console.log(`Loading ${bodyName}`);
|
||||
|
||||
const facts = database[bodyName];
|
||||
const facts = database[bodyName];
|
||||
|
||||
const distanceMultiplier = Math.pow(facts.distance, 1 / 3);
|
||||
const scaleMultiplier = Math.pow(facts.diameter, 1 / 3) / 25;
|
||||
const distanceMultiplier = Math.pow(facts.distance, 1 / 3);
|
||||
const scaleMultiplier = Math.pow(facts.diameter, 1 / 3) / 25;
|
||||
|
||||
const positionValue = { x: distanceMultiplier, y: 0, z: 0 };
|
||||
const scaleValue = { x: scaleMultiplier / 2, y: scaleMultiplier / 2, z: scaleMultiplier / 2 };
|
||||
const obliquityValue = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Forward(), facts.obliquity * MRESDK.DegreesToRadians);
|
||||
const inclinationValue = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Forward(), facts.inclination * MRESDK.DegreesToRadians);
|
||||
const positionValue = { x: distanceMultiplier, y: 0, z: 0 };
|
||||
const scaleValue = { x: scaleMultiplier / 2, y: scaleMultiplier / 2, z: scaleMultiplier / 2 };
|
||||
const obliquityValue = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Forward(), facts.obliquity * MRESDK.DegreesToRadians);
|
||||
const inclinationValue = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Forward(), facts.inclination * MRESDK.DegreesToRadians);
|
||||
|
||||
// Object layout for celestial body is:
|
||||
// inclination -- orbital plane. centered on sol and tilted
|
||||
// position -- position of center of celestial body (orbits sol)
|
||||
// label -- centered above position. location of label.
|
||||
// obliquity0 -- centered on position. hidden node to account
|
||||
// for the fact that obliquity is a world-relative axis
|
||||
// obliquity1 -- centered on position. tilt of obliquity axis
|
||||
// model -- centered on position. the celestial body (rotates)
|
||||
try {
|
||||
const inclination = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-inclination`,
|
||||
transform: {
|
||||
app: { rotation: inclinationValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const position = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-position`,
|
||||
parentId: inclination.id,
|
||||
transform: {
|
||||
local: { position: positionValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const label = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-label`,
|
||||
parentId: position.id,
|
||||
transform: {
|
||||
local: { position: { y: 0.1 + Math.pow(scaleMultiplier, 1 / 2.5) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const obliquity0 = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-obliquity0`,
|
||||
parentId: position.id
|
||||
}
|
||||
});
|
||||
const obliquity1 = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-obliquity1`,
|
||||
parentId: obliquity0.id,
|
||||
transform: {
|
||||
local: { rotation: obliquityValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const model = MRESDK.Actor.CreateFromGLTF(this.context, {
|
||||
resourceUrl: `${this.baseUrl}/assets/${bodyName}.gltf`,
|
||||
colliderType: 'sphere',
|
||||
actor: {
|
||||
name: `${bodyName}-body`,
|
||||
parentId: obliquity1.id,
|
||||
transform: {
|
||||
local: { scale: scaleValue }
|
||||
}
|
||||
}
|
||||
// Object layout for celestial body is:
|
||||
// inclination -- orbital plane. centered on sol and tilted
|
||||
// position -- position of center of celestial body (orbits sol)
|
||||
// label -- centered above position. location of label.
|
||||
// obliquity0 -- centered on position. hidden node to account
|
||||
// for the fact that obliquity is a world-relative axis
|
||||
// obliquity1 -- centered on position. tilt of obliquity axis
|
||||
// model -- centered on position. the celestial body (rotates)
|
||||
try {
|
||||
const inclination = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-inclination`,
|
||||
transform: {
|
||||
app: { rotation: inclinationValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const position = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-position`,
|
||||
parentId: inclination.id,
|
||||
transform: {
|
||||
local: { position: positionValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const label = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-label`,
|
||||
parentId: position.id,
|
||||
transform: {
|
||||
local: { position: { y: 0.1 + Math.pow(scaleMultiplier, 1 / 2.5) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const obliquity0 = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-obliquity0`,
|
||||
parentId: position.id
|
||||
}
|
||||
});
|
||||
const obliquity1 = MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: `${bodyName}-obliquity1`,
|
||||
parentId: obliquity0.id,
|
||||
transform: {
|
||||
local: { rotation: obliquityValue }
|
||||
}
|
||||
}
|
||||
});
|
||||
const model = MRESDK.Actor.CreateFromGLTF(this.context, {
|
||||
resourceUrl: `${this.baseUrl}/assets/${bodyName}.gltf`,
|
||||
colliderType: 'sphere',
|
||||
actor: {
|
||||
name: `${bodyName}-body`,
|
||||
parentId: obliquity1.id,
|
||||
transform: {
|
||||
local: { scale: scaleValue }
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
label.enableText({
|
||||
contents: bodyName,
|
||||
height: 0.5,
|
||||
pixelsPerLine: 50,
|
||||
color: MRESDK.Color3.Yellow(),
|
||||
anchor: MRESDK.TextAnchorLocation.TopCenter,
|
||||
justify: MRESDK.TextJustify.Center,
|
||||
});
|
||||
label.enableText({
|
||||
contents: bodyName,
|
||||
height: 0.5,
|
||||
pixelsPerLine: 50,
|
||||
color: MRESDK.Color3.Yellow(),
|
||||
anchor: MRESDK.TextAnchorLocation.TopCenter,
|
||||
justify: MRESDK.TextJustify.Center,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
label.text.color = MRESDK.Color3.White();
|
||||
}, 5000);
|
||||
setTimeout(() => {
|
||||
label.text.color = MRESDK.Color3.White();
|
||||
}, 5000);
|
||||
|
||||
this.celestialBodies[bodyName] = {
|
||||
inclination,
|
||||
position,
|
||||
obliquity0,
|
||||
obliquity1,
|
||||
model
|
||||
} as CelestialBody;
|
||||
this.celestialBodies[bodyName] = {
|
||||
inclination,
|
||||
position,
|
||||
obliquity0,
|
||||
obliquity1,
|
||||
model
|
||||
} as CelestialBody;
|
||||
|
||||
this.createAnimations(bodyName);
|
||||
} catch (e) {
|
||||
console.log("createBody failed", bodyName, e);
|
||||
}
|
||||
}
|
||||
this.createAnimations(bodyName);
|
||||
} catch (e) {
|
||||
console.log("createBody failed", bodyName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private createAnimations(bodyName: string) {
|
||||
this.createAxialAnimation(bodyName);
|
||||
this.createOrbitalAnimation(bodyName);
|
||||
}
|
||||
private createAnimations(bodyName: string) {
|
||||
this.createAxialAnimation(bodyName);
|
||||
this.createOrbitalAnimation(bodyName);
|
||||
}
|
||||
|
||||
public readonly timeFactor = 40;
|
||||
public readonly axialKeyframeCount = 90;
|
||||
public readonly orbitalKeyframeCount = 90;
|
||||
public readonly timeFactor = 40;
|
||||
public readonly axialKeyframeCount = 90;
|
||||
public readonly orbitalKeyframeCount = 90;
|
||||
|
||||
private createAxialAnimation(bodyName: string) {
|
||||
const facts = database[bodyName];
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
private createAxialAnimation(bodyName: string) {
|
||||
const facts = database[bodyName];
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
|
||||
if (facts.day > 0) {
|
||||
const spin = facts.retrograde ? -1 : 1;
|
||||
// days = seconds (not in agreement with orbital animation)
|
||||
const axisTimeInSeconds = facts.day / this.timeFactor;
|
||||
const timeStep = axisTimeInSeconds / this.axialKeyframeCount;
|
||||
const keyframes: MRESDK.AnimationKeyframe[] = [];
|
||||
const angleStep = 360 / this.axialKeyframeCount;
|
||||
const initial = celestialBody.model.transform.local.rotation.clone();
|
||||
let value: Partial<MRESDK.ActorLike>;
|
||||
if (facts.day > 0) {
|
||||
const spin = facts.retrograde ? -1 : 1;
|
||||
// days = seconds (not in agreement with orbital animation)
|
||||
const axisTimeInSeconds = facts.day / this.timeFactor;
|
||||
const timeStep = axisTimeInSeconds / this.axialKeyframeCount;
|
||||
const keyframes: MRESDK.AnimationKeyframe[] = [];
|
||||
const angleStep = 360 / this.axialKeyframeCount;
|
||||
const initial = celestialBody.model.transform.local.rotation.clone();
|
||||
let value: Partial<MRESDK.ActorLike>;
|
||||
|
||||
for (let i = 0; i < this.axialKeyframeCount; ++i) {
|
||||
const rotDelta = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Up(), (-angleStep * i * spin) * MRESDK.DegreesToRadians);
|
||||
const rotation = initial.multiply(rotDelta);
|
||||
value = {
|
||||
transform: {
|
||||
local: { rotation }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: timeStep * i,
|
||||
value,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < this.axialKeyframeCount; ++i) {
|
||||
const rotDelta = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Up(), (-angleStep * i * spin) * MRESDK.DegreesToRadians);
|
||||
const rotation = initial.multiply(rotDelta);
|
||||
value = {
|
||||
transform: {
|
||||
local: { rotation }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: timeStep * i,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// Final frame
|
||||
value = {
|
||||
transform: {
|
||||
local: { rotation: celestialBody.model.transform.local.rotation }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: axisTimeInSeconds,
|
||||
value,
|
||||
});
|
||||
// Final frame
|
||||
value = {
|
||||
transform: {
|
||||
local: { rotation: celestialBody.model.transform.local.rotation }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: axisTimeInSeconds,
|
||||
value,
|
||||
});
|
||||
|
||||
// Create the animation on the actor
|
||||
celestialBody.model.createAnimation(
|
||||
`${bodyName}:axial`, {
|
||||
keyframes,
|
||||
events: [],
|
||||
wrapMode: MRESDK.AnimationWrapMode.Loop
|
||||
});
|
||||
}
|
||||
}
|
||||
// Create the animation on the actor
|
||||
celestialBody.model.createAnimation(
|
||||
`${bodyName}:axial`, {
|
||||
keyframes,
|
||||
events: [],
|
||||
wrapMode: MRESDK.AnimationWrapMode.Loop
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createOrbitalAnimation(bodyName: string) {
|
||||
const facts = database[bodyName];
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
private createOrbitalAnimation(bodyName: string) {
|
||||
const facts = database[bodyName];
|
||||
const celestialBody = this.celestialBodies[bodyName];
|
||||
|
||||
if (facts.year > 0) {
|
||||
// years = seconds (not in agreement with axial animation)
|
||||
const orbitTimeInSeconds = facts.year / this.timeFactor;
|
||||
const timeStep = orbitTimeInSeconds / this.orbitalKeyframeCount;
|
||||
const angleStep = 360 / this.orbitalKeyframeCount;
|
||||
const keyframes: MRESDK.AnimationKeyframe[] = [];
|
||||
const initial = celestialBody.position.transform.local.position.clone();
|
||||
let value: Partial<MRESDK.ActorLike>;
|
||||
if (facts.year > 0) {
|
||||
// years = seconds (not in agreement with axial animation)
|
||||
const orbitTimeInSeconds = facts.year / this.timeFactor;
|
||||
const timeStep = orbitTimeInSeconds / this.orbitalKeyframeCount;
|
||||
const angleStep = 360 / this.orbitalKeyframeCount;
|
||||
const keyframes: MRESDK.AnimationKeyframe[] = [];
|
||||
const initial = celestialBody.position.transform.local.position.clone();
|
||||
let value: Partial<MRESDK.ActorLike>;
|
||||
|
||||
for (let i = 0; i < this.orbitalKeyframeCount; ++i) {
|
||||
const rotDelta = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Up(), (-angleStep * i) * MRESDK.DegreesToRadians);
|
||||
const position = initial.rotateByQuaternionToRef(rotDelta, new MRESDK.Vector3());
|
||||
value = {
|
||||
transform: {
|
||||
local: { position }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: timeStep * i,
|
||||
value,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < this.orbitalKeyframeCount; ++i) {
|
||||
const rotDelta = MRESDK.Quaternion.RotationAxis(
|
||||
MRESDK.Vector3.Up(), (-angleStep * i) * MRESDK.DegreesToRadians);
|
||||
const position = initial.rotateByQuaternionToRef(rotDelta, new MRESDK.Vector3());
|
||||
value = {
|
||||
transform: {
|
||||
local: { position }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: timeStep * i,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// Final frame
|
||||
value = {
|
||||
transform: {
|
||||
local: { position: celestialBody.position.transform.local.position }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: orbitTimeInSeconds,
|
||||
value,
|
||||
});
|
||||
// Final frame
|
||||
value = {
|
||||
transform: {
|
||||
local: { position: celestialBody.position.transform.local.position }
|
||||
}
|
||||
};
|
||||
keyframes.push({
|
||||
time: orbitTimeInSeconds,
|
||||
value,
|
||||
});
|
||||
|
||||
// Create the animation on the actor
|
||||
celestialBody.position.createAnimation(
|
||||
`${bodyName}:orbital`, {
|
||||
keyframes,
|
||||
events: [],
|
||||
wrapMode: MRESDK.AnimationWrapMode.Loop
|
||||
});
|
||||
}
|
||||
}
|
||||
// Create the animation on the actor
|
||||
celestialBody.position.createAnimation(
|
||||
`${bodyName}:orbital`, {
|
||||
keyframes,
|
||||
events: [],
|
||||
wrapMode: MRESDK.AnimationWrapMode.Loop
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ dotenv.config();
|
|||
|
||||
// Start listening for connections, and serve static files
|
||||
const server = new WebHost({
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
});
|
||||
|
||||
// Handle new application sessions
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"forin": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
"tabs"
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
|
|
|
@ -4,346 +4,346 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
Actor,
|
||||
AnimationKeyframe,
|
||||
AnimationWrapMode,
|
||||
ButtonBehavior,
|
||||
Context,
|
||||
DegreesToRadians,
|
||||
PrimitiveShape,
|
||||
Quaternion,
|
||||
TextAnchorLocation,
|
||||
Vector3
|
||||
Actor,
|
||||
AnimationKeyframe,
|
||||
AnimationWrapMode,
|
||||
ButtonBehavior,
|
||||
Context,
|
||||
DegreesToRadians,
|
||||
PrimitiveShape,
|
||||
Quaternion,
|
||||
TextAnchorLocation,
|
||||
Vector3
|
||||
} from '@microsoft/mixed-reality-extension-sdk';
|
||||
|
||||
enum GameState {
|
||||
Intro,
|
||||
Play,
|
||||
Celebration
|
||||
Intro,
|
||||
Play,
|
||||
Celebration
|
||||
}
|
||||
|
||||
enum GamePiece {
|
||||
X,
|
||||
O
|
||||
X,
|
||||
O
|
||||
}
|
||||
|
||||
/**
|
||||
* The main class of this app. All the logic goes here.
|
||||
*/
|
||||
export default class TicTacToe {
|
||||
private text: Actor = null;
|
||||
private textAnchor: Actor = null;
|
||||
private light: Actor = null;
|
||||
private gameState: GameState;
|
||||
private text: Actor = null;
|
||||
private textAnchor: Actor = null;
|
||||
private light: Actor = null;
|
||||
private gameState: GameState;
|
||||
|
||||
private currentPlayerGamePiece: GamePiece;
|
||||
private nextPlayerGamePiece: GamePiece;
|
||||
private currentPlayerGamePiece: GamePiece;
|
||||
private nextPlayerGamePiece: GamePiece;
|
||||
|
||||
private boardState: GamePiece[];
|
||||
private boardState: GamePiece[];
|
||||
|
||||
private gamePieceActors: Actor[];
|
||||
private gamePieceActors: Actor[];
|
||||
|
||||
private victoryChecks = [
|
||||
[0 * 3 + 0, 0 * 3 + 1, 0 * 3 + 2],
|
||||
[1 * 3 + 0, 1 * 3 + 1, 1 * 3 + 2],
|
||||
[2 * 3 + 0, 2 * 3 + 1, 2 * 3 + 2],
|
||||
[0 * 3 + 0, 1 * 3 + 0, 2 * 3 + 0],
|
||||
[0 * 3 + 1, 1 * 3 + 1, 2 * 3 + 1],
|
||||
[0 * 3 + 2, 1 * 3 + 2, 2 * 3 + 2],
|
||||
[0 * 3 + 0, 1 * 3 + 1, 2 * 3 + 2],
|
||||
[2 * 3 + 0, 1 * 3 + 1, 0 * 3 + 2]
|
||||
];
|
||||
private victoryChecks = [
|
||||
[0 * 3 + 0, 0 * 3 + 1, 0 * 3 + 2],
|
||||
[1 * 3 + 0, 1 * 3 + 1, 1 * 3 + 2],
|
||||
[2 * 3 + 0, 2 * 3 + 1, 2 * 3 + 2],
|
||||
[0 * 3 + 0, 1 * 3 + 0, 2 * 3 + 0],
|
||||
[0 * 3 + 1, 1 * 3 + 1, 2 * 3 + 1],
|
||||
[0 * 3 + 2, 1 * 3 + 2, 2 * 3 + 2],
|
||||
[0 * 3 + 0, 1 * 3 + 1, 2 * 3 + 2],
|
||||
[2 * 3 + 0, 1 * 3 + 1, 0 * 3 + 2]
|
||||
];
|
||||
|
||||
constructor(private context: Context, private baseUrl: string) {
|
||||
this.context.onStarted(() => this.started());
|
||||
}
|
||||
constructor(private context: Context, private baseUrl: string) {
|
||||
this.context.onStarted(() => this.started());
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the context is "started", initialize the app.
|
||||
*/
|
||||
private async started() {
|
||||
// Create a new actor with no mesh, but some text.
|
||||
this.textAnchor = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: 'TextAnchor',
|
||||
transform: {
|
||||
app: { position: { x: 0, y: 1.2, z: 0 } }
|
||||
},
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Once the context is "started", initialize the app.
|
||||
*/
|
||||
private async started() {
|
||||
// Create a new actor with no mesh, but some text.
|
||||
this.textAnchor = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
name: 'TextAnchor',
|
||||
transform: {
|
||||
app: { position: { x: 0, y: 1.2, z: 0 } }
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.text = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: this.textAnchor.id,
|
||||
name: 'Text',
|
||||
transform: {
|
||||
local: { position: { x: 0, y: 0.0, z: -1.5 } }
|
||||
},
|
||||
text: {
|
||||
contents: "Tic-Tac-Toe!",
|
||||
anchor: TextAnchorLocation.MiddleCenter,
|
||||
color: { r: 30 / 255, g: 206 / 255, b: 213 / 255 },
|
||||
height: 0.3
|
||||
},
|
||||
}
|
||||
});
|
||||
this.light = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: this.text.id,
|
||||
name: 'Light',
|
||||
transform: {
|
||||
local: {
|
||||
position: { x: 0, y: 1.0, z: -0.5 },
|
||||
rotation: Quaternion.RotationAxis(Vector3.Left(), -45.0 * DegreesToRadians),
|
||||
}
|
||||
},
|
||||
light: {
|
||||
color: { r: 1, g: 0.6, b: 0.3 },
|
||||
type: 'spot',
|
||||
intensity: 20,
|
||||
range: 6,
|
||||
spotAngle: 45 * DegreesToRadians
|
||||
},
|
||||
this.text = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: this.textAnchor.id,
|
||||
name: 'Text',
|
||||
transform: {
|
||||
local: { position: { x: 0, y: 0.0, z: -1.5 } }
|
||||
},
|
||||
text: {
|
||||
contents: "Tic-Tac-Toe!",
|
||||
anchor: TextAnchorLocation.MiddleCenter,
|
||||
color: { r: 30 / 255, g: 206 / 255, b: 213 / 255 },
|
||||
height: 0.3
|
||||
},
|
||||
}
|
||||
});
|
||||
this.light = Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: this.text.id,
|
||||
name: 'Light',
|
||||
transform: {
|
||||
local: {
|
||||
position: { x: 0, y: 1.0, z: -0.5 },
|
||||
rotation: Quaternion.RotationAxis(Vector3.Left(), -45.0 * DegreesToRadians),
|
||||
}
|
||||
},
|
||||
light: {
|
||||
color: { r: 1, g: 0.6, b: 0.3 },
|
||||
type: 'spot',
|
||||
intensity: 20,
|
||||
range: 6,
|
||||
spotAngle: 45 * DegreesToRadians
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Here we create an animation on our text actor. Animations have three mandatory arguments:
|
||||
// a name, an array of keyframes, and an array of events.
|
||||
this.textAnchor.createAnimation(
|
||||
// The name is a unique identifier for this animation. We'll pass it to "startAnimation" later.
|
||||
"Spin", {
|
||||
// Keyframes define the timeline for the animation: where the actor should be, and when.
|
||||
// We're calling the generateSpinKeyframes function to produce a simple 20-second revolution.
|
||||
keyframes: this.generateSpinKeyframes(20, Vector3.Up()),
|
||||
// Events are points of interest during the animation. The animating actor will emit a given
|
||||
// named event at the given timestamp with a given string value as an argument.
|
||||
events: [],
|
||||
// Here we create an animation on our text actor. Animations have three mandatory arguments:
|
||||
// a name, an array of keyframes, and an array of events.
|
||||
this.textAnchor.createAnimation(
|
||||
// The name is a unique identifier for this animation. We'll pass it to "startAnimation" later.
|
||||
"Spin", {
|
||||
// Keyframes define the timeline for the animation: where the actor should be, and when.
|
||||
// We're calling the generateSpinKeyframes function to produce a simple 20-second revolution.
|
||||
keyframes: this.generateSpinKeyframes(20, Vector3.Up()),
|
||||
// Events are points of interest during the animation. The animating actor will emit a given
|
||||
// named event at the given timestamp with a given string value as an argument.
|
||||
events: [],
|
||||
|
||||
// Optionally, we also repeat the animation infinitely.
|
||||
wrapMode: AnimationWrapMode.Loop
|
||||
});
|
||||
// Optionally, we also repeat the animation infinitely.
|
||||
wrapMode: AnimationWrapMode.Loop
|
||||
});
|
||||
|
||||
for (let tileIndexX = 0; tileIndexX < 3; tileIndexX++) {
|
||||
for (let tileIndexZ = 0; tileIndexZ < 3; tileIndexZ++) {
|
||||
// Load a glTF model
|
||||
const cube = Actor.CreateFromGLTF(this.context, {
|
||||
// at the given URL
|
||||
resourceUrl: `${this.baseUrl}/altspace-cube.glb`,
|
||||
// and spawn box colliders around the meshes.
|
||||
colliderType: 'box',
|
||||
// Also apply the following generic actor properties.
|
||||
actor: {
|
||||
name: 'Altspace Cube',
|
||||
transform: {
|
||||
app: {
|
||||
position: { x: (tileIndexX) - 1.0, y: 0.5, z: (tileIndexZ) - 1.0 },
|
||||
},
|
||||
local: { scale: { x: 0.4, y: 0.4, z: 0.4 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
for (let tileIndexX = 0; tileIndexX < 3; tileIndexX++) {
|
||||
for (let tileIndexZ = 0; tileIndexZ < 3; tileIndexZ++) {
|
||||
// Load a glTF model
|
||||
const cube = Actor.CreateFromGLTF(this.context, {
|
||||
// at the given URL
|
||||
resourceUrl: `${this.baseUrl}/altspace-cube.glb`,
|
||||
// and spawn box colliders around the meshes.
|
||||
colliderType: 'box',
|
||||
// Also apply the following generic actor properties.
|
||||
actor: {
|
||||
name: 'Altspace Cube',
|
||||
transform: {
|
||||
app: {
|
||||
position: { x: (tileIndexX) - 1.0, y: 0.5, z: (tileIndexZ) - 1.0 },
|
||||
},
|
||||
local: { scale: { x: 0.4, y: 0.4, z: 0.4 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create some animations on the cube.
|
||||
cube.createAnimation(
|
||||
'GrowIn', {
|
||||
keyframes: this.growAnimationData,
|
||||
events: []
|
||||
});
|
||||
// Create some animations on the cube.
|
||||
cube.createAnimation(
|
||||
'GrowIn', {
|
||||
keyframes: this.growAnimationData,
|
||||
events: []
|
||||
});
|
||||
|
||||
cube.createAnimation(
|
||||
'ShrinkOut', {
|
||||
keyframes: this.shrinkAnimationData,
|
||||
events: []
|
||||
});
|
||||
cube.createAnimation(
|
||||
'ShrinkOut', {
|
||||
keyframes: this.shrinkAnimationData,
|
||||
events: []
|
||||
});
|
||||
|
||||
cube.createAnimation(
|
||||
'DoAFlip', {
|
||||
keyframes: this.generateSpinKeyframes(1.0, Vector3.Right()),
|
||||
events: []
|
||||
});
|
||||
cube.createAnimation(
|
||||
'DoAFlip', {
|
||||
keyframes: this.generateSpinKeyframes(1.0, Vector3.Right()),
|
||||
events: []
|
||||
});
|
||||
|
||||
// Set up cursor interaction. We add the input behavior ButtonBehavior to the cube.
|
||||
// Button behaviors have two pairs of events: hover start/stop, and click start/stop.
|
||||
const buttonBehavior = cube.setBehavior(ButtonBehavior);
|
||||
// Set up cursor interaction. We add the input behavior ButtonBehavior to the cube.
|
||||
// Button behaviors have two pairs of events: hover start/stop, and click start/stop.
|
||||
const buttonBehavior = cube.setBehavior(ButtonBehavior);
|
||||
|
||||
// Trigger the grow/shrink animations on hover.
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
if (this.gameState === GameState.Play &&
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
cube.enableAnimation('GrowIn');
|
||||
}
|
||||
});
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
if (this.gameState === GameState.Play &&
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
cube.enableAnimation('ShrinkOut');
|
||||
}
|
||||
});
|
||||
// Trigger the grow/shrink animations on hover.
|
||||
buttonBehavior.onHover('enter', () => {
|
||||
if (this.gameState === GameState.Play &&
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
cube.enableAnimation('GrowIn');
|
||||
}
|
||||
});
|
||||
buttonBehavior.onHover('exit', () => {
|
||||
if (this.gameState === GameState.Play &&
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
cube.enableAnimation('ShrinkOut');
|
||||
}
|
||||
});
|
||||
|
||||
buttonBehavior.onClick(_ => {
|
||||
switch (this.gameState) {
|
||||
case GameState.Intro:
|
||||
this.beginGameStatePlay();
|
||||
cube.enableAnimation('GrowIn');
|
||||
break;
|
||||
case GameState.Play:
|
||||
// When clicked, put down a tile, and do a victory check
|
||||
if (this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
console.log("Putting an " + GamePiece[this.currentPlayerGamePiece] +
|
||||
" on: (" + tileIndexX + "," + tileIndexZ + ")");
|
||||
const gamePiecePosition: Vector3 = new Vector3(
|
||||
cube.transform.local.position.x,
|
||||
cube.transform.local.position.y + 0.55,
|
||||
cube.transform.local.position.z);
|
||||
if (this.currentPlayerGamePiece === GamePiece.O) {
|
||||
this.gamePieceActors.push(Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: PrimitiveShape.Cylinder,
|
||||
dimensions: { x: 0, y: 0.2, z: 0 },
|
||||
radius: 0.4,
|
||||
uSegments: 16,
|
||||
},
|
||||
actor: {
|
||||
name: 'O',
|
||||
transform: {
|
||||
local: { position: gamePiecePosition }
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
this.gamePieceActors.push(Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: PrimitiveShape.Box,
|
||||
dimensions: { x: 0.70, y: 0.2, z: 0.70 }
|
||||
},
|
||||
actor: {
|
||||
name: 'X',
|
||||
transform: {
|
||||
local: { position: gamePiecePosition }
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] = this.currentPlayerGamePiece;
|
||||
cube.disableAnimation('GrowIn');
|
||||
cube.enableAnimation('ShrinkOut');
|
||||
buttonBehavior.onClick(_ => {
|
||||
switch (this.gameState) {
|
||||
case GameState.Intro:
|
||||
this.beginGameStatePlay();
|
||||
cube.enableAnimation('GrowIn');
|
||||
break;
|
||||
case GameState.Play:
|
||||
// When clicked, put down a tile, and do a victory check
|
||||
if (this.boardState[tileIndexX * 3 + tileIndexZ] === undefined) {
|
||||
console.log("Putting an " + GamePiece[this.currentPlayerGamePiece] +
|
||||
" on: (" + tileIndexX + "," + tileIndexZ + ")");
|
||||
const gamePiecePosition: Vector3 = new Vector3(
|
||||
cube.transform.local.position.x,
|
||||
cube.transform.local.position.y + 0.55,
|
||||
cube.transform.local.position.z);
|
||||
if (this.currentPlayerGamePiece === GamePiece.O) {
|
||||
this.gamePieceActors.push(Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: PrimitiveShape.Cylinder,
|
||||
dimensions: { x: 0, y: 0.2, z: 0 },
|
||||
radius: 0.4,
|
||||
uSegments: 16,
|
||||
},
|
||||
actor: {
|
||||
name: 'O',
|
||||
transform: {
|
||||
local: { position: gamePiecePosition }
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
this.gamePieceActors.push(Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: PrimitiveShape.Box,
|
||||
dimensions: { x: 0.70, y: 0.2, z: 0.70 }
|
||||
},
|
||||
actor: {
|
||||
name: 'X',
|
||||
transform: {
|
||||
local: { position: gamePiecePosition }
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.boardState[tileIndexX * 3 + tileIndexZ] = this.currentPlayerGamePiece;
|
||||
cube.disableAnimation('GrowIn');
|
||||
cube.enableAnimation('ShrinkOut');
|
||||
|
||||
const tempGamePiece = this.currentPlayerGamePiece;
|
||||
this.currentPlayerGamePiece = this.nextPlayerGamePiece;
|
||||
this.nextPlayerGamePiece = tempGamePiece;
|
||||
const tempGamePiece = this.currentPlayerGamePiece;
|
||||
this.currentPlayerGamePiece = this.nextPlayerGamePiece;
|
||||
this.nextPlayerGamePiece = tempGamePiece;
|
||||
|
||||
this.text.text.contents = "Next Piece: " + GamePiece[this.currentPlayerGamePiece];
|
||||
this.text.text.contents = "Next Piece: " + GamePiece[this.currentPlayerGamePiece];
|
||||
|
||||
for (const victoryCheck of this.victoryChecks) {
|
||||
if (this.boardState[victoryCheck[0]] !== undefined &&
|
||||
this.boardState[victoryCheck[0]] === this.boardState[victoryCheck[1]] &&
|
||||
this.boardState[victoryCheck[0]] === this.boardState[victoryCheck[2]]) {
|
||||
this.beginGameStateCelebration(tempGamePiece);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const victoryCheck of this.victoryChecks) {
|
||||
if (this.boardState[victoryCheck[0]] !== undefined &&
|
||||
this.boardState[victoryCheck[0]] === this.boardState[victoryCheck[1]] &&
|
||||
this.boardState[victoryCheck[0]] === this.boardState[victoryCheck[2]]) {
|
||||
this.beginGameStateCelebration(tempGamePiece);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let hasEmptySpace = false;
|
||||
for (let i = 0; i < 3 * 3; i++) {
|
||||
if (this.boardState[i] === undefined) {
|
||||
hasEmptySpace = true;
|
||||
}
|
||||
}
|
||||
if (hasEmptySpace === false) {
|
||||
this.beginGameStateCelebration(undefined);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GameState.Celebration:
|
||||
default:
|
||||
this.beginGameStateIntro();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Now that the text and its animation are all being set up, we can start playing
|
||||
// the animation.
|
||||
this.textAnchor.enableAnimation('Spin');
|
||||
this.beginGameStateIntro();
|
||||
}
|
||||
let hasEmptySpace = false;
|
||||
for (let i = 0; i < 3 * 3; i++) {
|
||||
if (this.boardState[i] === undefined) {
|
||||
hasEmptySpace = true;
|
||||
}
|
||||
}
|
||||
if (hasEmptySpace === false) {
|
||||
this.beginGameStateCelebration(undefined);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GameState.Celebration:
|
||||
default:
|
||||
this.beginGameStateIntro();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Now that the text and its animation are all being set up, we can start playing
|
||||
// the animation.
|
||||
this.textAnchor.enableAnimation('Spin');
|
||||
this.beginGameStateIntro();
|
||||
}
|
||||
|
||||
private beginGameStateCelebration(winner: GamePiece) {
|
||||
console.log("BeginGameState Celebration");
|
||||
this.gameState = GameState.Celebration;
|
||||
this.light.light.color = { r: 0.3, g: 1.0, b: 0.3 };
|
||||
private beginGameStateCelebration(winner: GamePiece) {
|
||||
console.log("BeginGameState Celebration");
|
||||
this.gameState = GameState.Celebration;
|
||||
this.light.light.color = { r: 0.3, g: 1.0, b: 0.3 };
|
||||
|
||||
if (winner === undefined) {
|
||||
console.log("Tie");
|
||||
this.text.text.contents = "Tie";
|
||||
} else {
|
||||
console.log("Winner: " + GamePiece[winner]);
|
||||
this.text.text.contents = "Winner: " + GamePiece[winner];
|
||||
}
|
||||
}
|
||||
if (winner === undefined) {
|
||||
console.log("Tie");
|
||||
this.text.text.contents = "Tie";
|
||||
} else {
|
||||
console.log("Winner: " + GamePiece[winner]);
|
||||
this.text.text.contents = "Winner: " + GamePiece[winner];
|
||||
}
|
||||
}
|
||||
|
||||
private beginGameStateIntro() {
|
||||
console.log("BeginGameState Intro");
|
||||
this.gameState = GameState.Intro;
|
||||
this.text.text.contents = "Tic-Tac-Toe\nClick To Play";
|
||||
private beginGameStateIntro() {
|
||||
console.log("BeginGameState Intro");
|
||||
this.gameState = GameState.Intro;
|
||||
this.text.text.contents = "Tic-Tac-Toe\nClick To Play";
|
||||
|
||||
this.currentPlayerGamePiece = GamePiece.X;
|
||||
this.nextPlayerGamePiece = GamePiece.O;
|
||||
this.boardState = [];
|
||||
this.light.light.color = { r: 1, g: 0.6, b: 0.3 };
|
||||
this.currentPlayerGamePiece = GamePiece.X;
|
||||
this.nextPlayerGamePiece = GamePiece.O;
|
||||
this.boardState = [];
|
||||
this.light.light.color = { r: 1, g: 0.6, b: 0.3 };
|
||||
|
||||
if (this.gamePieceActors !== undefined) {
|
||||
for (const actor of this.gamePieceActors) {
|
||||
actor.destroy();
|
||||
}
|
||||
}
|
||||
this.gamePieceActors = [];
|
||||
}
|
||||
if (this.gamePieceActors !== undefined) {
|
||||
for (const actor of this.gamePieceActors) {
|
||||
actor.destroy();
|
||||
}
|
||||
}
|
||||
this.gamePieceActors = [];
|
||||
}
|
||||
|
||||
private beginGameStatePlay() {
|
||||
console.log("BeginGameState Play");
|
||||
this.gameState = GameState.Play;
|
||||
this.text.text.contents = "First Piece: " + GamePiece[this.currentPlayerGamePiece];
|
||||
}
|
||||
private beginGameStatePlay() {
|
||||
console.log("BeginGameState Play");
|
||||
this.gameState = GameState.Play;
|
||||
this.text.text.contents = "First Piece: " + GamePiece[this.currentPlayerGamePiece];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate keyframe data for a simple spin animation.
|
||||
* @param duration The length of time in seconds it takes to complete a full revolution.
|
||||
* @param axis The axis of rotation in local space.
|
||||
*/
|
||||
private generateSpinKeyframes(duration: number, axis: Vector3): AnimationKeyframe[] {
|
||||
return [{
|
||||
time: 0 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 0) } } }
|
||||
}, {
|
||||
time: 0.25 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 0.5 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI) } } }
|
||||
}, {
|
||||
time: 0.75 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 3 * Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 1 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 2 * Math.PI) } } }
|
||||
}];
|
||||
}
|
||||
/**
|
||||
* Generate keyframe data for a simple spin animation.
|
||||
* @param duration The length of time in seconds it takes to complete a full revolution.
|
||||
* @param axis The axis of rotation in local space.
|
||||
*/
|
||||
private generateSpinKeyframes(duration: number, axis: Vector3): AnimationKeyframe[] {
|
||||
return [{
|
||||
time: 0 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 0) } } }
|
||||
}, {
|
||||
time: 0.25 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 0.5 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, Math.PI) } } }
|
||||
}, {
|
||||
time: 0.75 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 3 * Math.PI / 2) } } }
|
||||
}, {
|
||||
time: 1 * duration,
|
||||
value: { transform: { local: { rotation: Quaternion.RotationAxis(axis, 2 * Math.PI) } } }
|
||||
}];
|
||||
}
|
||||
|
||||
private growAnimationData: AnimationKeyframe[] = [{
|
||||
time: 0,
|
||||
value: { transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }
|
||||
}, {
|
||||
time: 0.3,
|
||||
value: { transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }
|
||||
}];
|
||||
private growAnimationData: AnimationKeyframe[] = [{
|
||||
time: 0,
|
||||
value: { transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }
|
||||
}, {
|
||||
time: 0.3,
|
||||
value: { transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }
|
||||
}];
|
||||
|
||||
private shrinkAnimationData: AnimationKeyframe[] = [{
|
||||
time: 0,
|
||||
value: { transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }
|
||||
}, {
|
||||
time: 0.3,
|
||||
value: { transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }
|
||||
}];
|
||||
private shrinkAnimationData: AnimationKeyframe[] = [{
|
||||
time: 0,
|
||||
value: { transform: { local: { scale: { x: 0.5, y: 0.5, z: 0.5 } } } }
|
||||
}, {
|
||||
time: 0.3,
|
||||
value: { transform: { local: { scale: { x: 0.4, y: 0.4, z: 0.4 } } } }
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ dotenv.config();
|
|||
|
||||
// Start listening for connections, and serve static files
|
||||
const server = new WebHost({
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
});
|
||||
|
||||
// Handle new application sessions
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"forin": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
"tabs"
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
|
|
|
@ -9,30 +9,30 @@ import * as MRESDK from '@microsoft/mixed-reality-extension-sdk';
|
|||
* The structure of a hat entry in the hat database.
|
||||
*/
|
||||
type HatDescriptor = {
|
||||
displayName: string;
|
||||
resourceName: string;
|
||||
scale: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
rotation: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
displayName: string;
|
||||
resourceName: string;
|
||||
scale: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
rotation: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The structure of the hat database.
|
||||
*/
|
||||
type HatDatabase = {
|
||||
[key: string]: HatDescriptor;
|
||||
[key: string]: HatDescriptor;
|
||||
};
|
||||
|
||||
// Load the database of hats.
|
||||
|
@ -43,171 +43,171 @@ const HatDatabase: HatDatabase = require('../public/hats.json');
|
|||
* WearAHat Application - Showcasing avatar attachments.
|
||||
*/
|
||||
export default class WearAHat {
|
||||
// Container for preloaded hat prefabs.
|
||||
private assets: MRESDK.AssetContainer;
|
||||
private prefabs: { [key: string]: MRESDK.Prefab } = {};
|
||||
// Container for instantiated hats.
|
||||
private attachedHats: { [key: string]: MRESDK.Actor } = {};
|
||||
// Container for preloaded hat prefabs.
|
||||
private assets: MRESDK.AssetContainer;
|
||||
private prefabs: { [key: string]: MRESDK.Prefab } = {};
|
||||
// Container for instantiated hats.
|
||||
private attachedHats: { [key: string]: MRESDK.Actor } = {};
|
||||
|
||||
/**
|
||||
* Constructs a new instance of this class.
|
||||
* @param context The MRE SDK context.
|
||||
* @param baseUrl The baseUrl to this project's `./public` folder.
|
||||
*/
|
||||
constructor(private context: MRESDK.Context, private baseUrl: string) {
|
||||
this.assets = new MRESDK.AssetContainer(context);
|
||||
// Hook the context events we're interested in.
|
||||
this.context.onStarted(() => this.started());
|
||||
this.context.onUserLeft(user => this.userLeft(user));
|
||||
}
|
||||
/**
|
||||
* Constructs a new instance of this class.
|
||||
* @param context The MRE SDK context.
|
||||
* @param baseUrl The baseUrl to this project's `./public` folder.
|
||||
*/
|
||||
constructor(private context: MRESDK.Context, private baseUrl: string) {
|
||||
this.assets = new MRESDK.AssetContainer(context);
|
||||
// Hook the context events we're interested in.
|
||||
this.context.onStarted(() => this.started());
|
||||
this.context.onUserLeft(user => this.userLeft(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Hats application session starts up.
|
||||
*/
|
||||
private async started() {
|
||||
// Preload all the hat models.
|
||||
await this.preloadHats();
|
||||
// Show the hat menu.
|
||||
this.showHatMenu();
|
||||
}
|
||||
/**
|
||||
* Called when a Hats application session starts up.
|
||||
*/
|
||||
private async started() {
|
||||
// Preload all the hat models.
|
||||
await this.preloadHats();
|
||||
// Show the hat menu.
|
||||
this.showHatMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a user leaves the application (probably left the Altspace world where this app is running).
|
||||
* @param user The user that left the building.
|
||||
*/
|
||||
private userLeft(user: MRESDK.User) {
|
||||
// If the user was wearing a hat, destroy it. Otherwise it would be
|
||||
// orphaned in the world.
|
||||
if (this.attachedHats[user.id]) this.attachedHats[user.id].destroy();
|
||||
delete this.attachedHats[user.id];
|
||||
}
|
||||
/**
|
||||
* Called when a user leaves the application (probably left the Altspace world where this app is running).
|
||||
* @param user The user that left the building.
|
||||
*/
|
||||
private userLeft(user: MRESDK.User) {
|
||||
// If the user was wearing a hat, destroy it. Otherwise it would be
|
||||
// orphaned in the world.
|
||||
if (this.attachedHats[user.id]) this.attachedHats[user.id].destroy();
|
||||
delete this.attachedHats[user.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a menu of hat selections.
|
||||
*/
|
||||
private showHatMenu() {
|
||||
// Create a parent object for all the menu items.
|
||||
const menu = MRESDK.Actor.CreateEmpty(this.context);
|
||||
let y = 0.3;
|
||||
/**
|
||||
* Show a menu of hat selections.
|
||||
*/
|
||||
private showHatMenu() {
|
||||
// Create a parent object for all the menu items.
|
||||
const menu = MRESDK.Actor.CreateEmpty(this.context);
|
||||
let y = 0.3;
|
||||
|
||||
// Loop over the hat database, creating a menu item for each entry.
|
||||
for (const hatId of Object.keys(HatDatabase)) {
|
||||
// Create a clickable button.
|
||||
const button = MRESDK.Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: MRESDK.PrimitiveShape.Box,
|
||||
dimensions: { x: 0.3, y: 0.3, z: 0.01 }
|
||||
},
|
||||
addCollider: true,
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: hatId,
|
||||
transform: {
|
||||
local: { position: { x: 0, y, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Loop over the hat database, creating a menu item for each entry.
|
||||
for (const hatId of Object.keys(HatDatabase)) {
|
||||
// Create a clickable button.
|
||||
const button = MRESDK.Actor.CreatePrimitive(this.context, {
|
||||
definition: {
|
||||
shape: MRESDK.PrimitiveShape.Box,
|
||||
dimensions: { x: 0.3, y: 0.3, z: 0.01 }
|
||||
},
|
||||
addCollider: true,
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: hatId,
|
||||
transform: {
|
||||
local: { position: { x: 0, y, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set a click handler on the button.
|
||||
button.setBehavior(MRESDK.ButtonBehavior)
|
||||
.onClick(user => this.wearHat(hatId, user.id));
|
||||
// Set a click handler on the button.
|
||||
button.setBehavior(MRESDK.ButtonBehavior)
|
||||
.onClick(user => this.wearHat(hatId, user.id));
|
||||
|
||||
// Create a label for the menu entry.
|
||||
MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: 'label',
|
||||
text: {
|
||||
contents: HatDatabase[hatId].displayName,
|
||||
height: 0.5,
|
||||
anchor: MRESDK.TextAnchorLocation.MiddleLeft
|
||||
},
|
||||
transform: {
|
||||
local: { position: { x: 0.5, y, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
y = y + 0.5;
|
||||
}
|
||||
// Create a label for the menu entry.
|
||||
MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: 'label',
|
||||
text: {
|
||||
contents: HatDatabase[hatId].displayName,
|
||||
height: 0.5,
|
||||
anchor: MRESDK.TextAnchorLocation.MiddleLeft
|
||||
},
|
||||
transform: {
|
||||
local: { position: { x: 0.5, y, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
y = y + 0.5;
|
||||
}
|
||||
|
||||
// Create a label for the menu title.
|
||||
MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: 'label',
|
||||
text: {
|
||||
contents: ''.padStart(8, ' ') + "Wear a Hat",
|
||||
height: 0.8,
|
||||
anchor: MRESDK.TextAnchorLocation.MiddleCenter,
|
||||
color: MRESDK.Color3.Yellow()
|
||||
},
|
||||
transform: {
|
||||
local: { position: { x: 0.5, y: y + 0.25, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Create a label for the menu title.
|
||||
MRESDK.Actor.CreateEmpty(this.context, {
|
||||
actor: {
|
||||
parentId: menu.id,
|
||||
name: 'label',
|
||||
text: {
|
||||
contents: ''.padStart(8, ' ') + "Wear a Hat",
|
||||
height: 0.8,
|
||||
anchor: MRESDK.TextAnchorLocation.MiddleCenter,
|
||||
color: MRESDK.Color3.Yellow()
|
||||
},
|
||||
transform: {
|
||||
local: { position: { x: 0.5, y: y + 0.25, z: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload all hat resources. This makes instantiating them faster and more efficient.
|
||||
*/
|
||||
private preloadHats() {
|
||||
// Loop over the hat database, preloading each hat resource.
|
||||
// Return a promise of all the in-progress load promises. This
|
||||
// allows the caller to wait until all hats are done preloading
|
||||
// before continuing.
|
||||
return Promise.all(
|
||||
Object.keys(HatDatabase).map(hatId => {
|
||||
const hatRecord = HatDatabase[hatId];
|
||||
if (hatRecord.resourceName) {
|
||||
return this.assets.loadGltf(
|
||||
`${this.baseUrl}/${hatRecord.resourceName}`)
|
||||
.then(assets => {
|
||||
this.prefabs[hatId] = assets.find(a => a.prefab !== null) as MRESDK.Prefab;
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Preload all hat resources. This makes instantiating them faster and more efficient.
|
||||
*/
|
||||
private preloadHats() {
|
||||
// Loop over the hat database, preloading each hat resource.
|
||||
// Return a promise of all the in-progress load promises. This
|
||||
// allows the caller to wait until all hats are done preloading
|
||||
// before continuing.
|
||||
return Promise.all(
|
||||
Object.keys(HatDatabase).map(hatId => {
|
||||
const hatRecord = HatDatabase[hatId];
|
||||
if (hatRecord.resourceName) {
|
||||
return this.assets.loadGltf(
|
||||
`${this.baseUrl}/${hatRecord.resourceName}`)
|
||||
.then(assets => {
|
||||
this.prefabs[hatId] = assets.find(a => a.prefab !== null) as MRESDK.Prefab;
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a hat and attach it to the avatar's head.
|
||||
* @param hatId The id of the hat in the hat database.
|
||||
* @param userId The id of the user we will attach the hat to.
|
||||
*/
|
||||
private wearHat(hatId: string, userId: string) {
|
||||
// If the user is wearing a hat, destroy it.
|
||||
if (this.attachedHats[userId]) this.attachedHats[userId].destroy();
|
||||
delete this.attachedHats[userId];
|
||||
/**
|
||||
* Instantiate a hat and attach it to the avatar's head.
|
||||
* @param hatId The id of the hat in the hat database.
|
||||
* @param userId The id of the user we will attach the hat to.
|
||||
*/
|
||||
private wearHat(hatId: string, userId: string) {
|
||||
// If the user is wearing a hat, destroy it.
|
||||
if (this.attachedHats[userId]) this.attachedHats[userId].destroy();
|
||||
delete this.attachedHats[userId];
|
||||
|
||||
const hatRecord = HatDatabase[hatId];
|
||||
const hatRecord = HatDatabase[hatId];
|
||||
|
||||
// If the user selected 'none', then early out.
|
||||
if (!hatRecord.resourceName) {
|
||||
return;
|
||||
}
|
||||
// If the user selected 'none', then early out.
|
||||
if (!hatRecord.resourceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the hat model and attach it to the avatar's head.
|
||||
this.attachedHats[userId] = MRESDK.Actor.CreateFromPrefab(this.context, {
|
||||
prefabId: this.prefabs[hatId].id,
|
||||
actor: {
|
||||
transform: {
|
||||
local: {
|
||||
position: hatRecord.position,
|
||||
rotation: MRESDK.Quaternion.FromEulerAngles(
|
||||
hatRecord.rotation.x * MRESDK.DegreesToRadians,
|
||||
hatRecord.rotation.y * MRESDK.DegreesToRadians,
|
||||
hatRecord.rotation.z * MRESDK.DegreesToRadians),
|
||||
scale: hatRecord.scale,
|
||||
}
|
||||
},
|
||||
attachment: {
|
||||
attachPoint: 'head',
|
||||
userId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Create the hat model and attach it to the avatar's head.
|
||||
this.attachedHats[userId] = MRESDK.Actor.CreateFromPrefab(this.context, {
|
||||
prefabId: this.prefabs[hatId].id,
|
||||
actor: {
|
||||
transform: {
|
||||
local: {
|
||||
position: hatRecord.position,
|
||||
rotation: MRESDK.Quaternion.FromEulerAngles(
|
||||
hatRecord.rotation.x * MRESDK.DegreesToRadians,
|
||||
hatRecord.rotation.y * MRESDK.DegreesToRadians,
|
||||
hatRecord.rotation.z * MRESDK.DegreesToRadians),
|
||||
scale: hatRecord.scale,
|
||||
}
|
||||
},
|
||||
attachment: {
|
||||
attachPoint: 'head',
|
||||
userId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ dotenv.config();
|
|||
|
||||
// Start listening for connections, and serve static files
|
||||
const server = new WebHost({
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
// baseUrl: 'http://<ngrok-id>.ngrok.io',
|
||||
baseDir: resolvePath(__dirname, '../public')
|
||||
});
|
||||
|
||||
// Handle new application sessions
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"forin": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
"tabs"
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
|
|
|
@ -2,7 +2,7 @@ var shell = require('shelljs');
|
|||
var forEachSample = require('./foreach-sample');
|
||||
|
||||
forEachSample((sampleDir) => {
|
||||
var cmd = `pushd ${sampleDir} && npm run build && popd`;
|
||||
console.log(cmd);
|
||||
shell.exec(cmd);
|
||||
var cmd = `pushd ${sampleDir} && npm run build && popd`;
|
||||
console.log(cmd);
|
||||
shell.exec(cmd);
|
||||
});
|
||||
|
|
|
@ -4,54 +4,54 @@ var path = require('path');
|
|||
var forEachSample = require('./foreach-sample');
|
||||
|
||||
const helpText = "Usage: change-sdk-source.js <sdk_source_type>\n\n\tWhere <sdk_source_type> is either: npm or sdk-source\n\n" +
|
||||
"\tEnsure that there is a 'sdk-path-config.json' containing { \"sdkPath\": \"<path_to_sdk_directory>\" }\n";
|
||||
"\tEnsure that there is a 'sdk-path-config.json' containing { \"sdkPath\": \"<path_to_sdk_directory>\" }\n";
|
||||
|
||||
function printError(errorMessage) {
|
||||
console.error(`ERROR: ${errorMessage}\n`);
|
||||
console.error(helpText);
|
||||
process.exit(1);
|
||||
console.error(`ERROR: ${errorMessage}\n`);
|
||||
console.error(helpText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(helpText);
|
||||
process.exit();
|
||||
console.log(helpText);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function updateSdkTarget(sampleDir, sdkPath) {
|
||||
// update environment to be linked to the local source package, or to be the npm package.
|
||||
if (args[0] === 'npm') {
|
||||
console.log(`\nUpdating SDK Target: ${sampleDir} -> npm package`);
|
||||
console.log(`pushd ${sampleDir} && npm unlink ${sdkPath} && npm install && popd`);
|
||||
shell.exec(`pushd ${sampleDir} && npm unlink ${sdkPath} && npm install && popd`);
|
||||
} else if (args[0] === 'source') {
|
||||
console.log(`\nUpdating SDK Target: ${sampleDir} -> ${sdkPath}`);
|
||||
console.log(`pushd ${sampleDir} && npm link ${sdkPath} && popd`);
|
||||
shell.exec(`pushd ${sampleDir} && npm link ${sdkPath} && popd`);
|
||||
} else {
|
||||
printError("Invalid command.");
|
||||
}
|
||||
// update environment to be linked to the local source package, or to be the npm package.
|
||||
if (args[0] === 'npm') {
|
||||
console.log(`\nUpdating SDK Target: ${sampleDir} -> npm package`);
|
||||
console.log(`pushd ${sampleDir} && npm unlink ${sdkPath} && npm install && popd`);
|
||||
shell.exec(`pushd ${sampleDir} && npm unlink ${sdkPath} && npm install && popd`);
|
||||
} else if (args[0] === 'source') {
|
||||
console.log(`\nUpdating SDK Target: ${sampleDir} -> ${sdkPath}`);
|
||||
console.log(`pushd ${sampleDir} && npm link ${sdkPath} && popd`);
|
||||
shell.exec(`pushd ${sampleDir} && npm link ${sdkPath} && popd`);
|
||||
} else {
|
||||
printError("Invalid command.");
|
||||
}
|
||||
}
|
||||
|
||||
var args = process.argv.slice(2);
|
||||
console.log();
|
||||
if (args.find(arg => { return (arg === '-h' || arg === '--help') }) !== undefined) {
|
||||
printHelp();
|
||||
printHelp();
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
printError("Must supply an argument of either npm or sdk-source.");
|
||||
printError("Must supply an argument of either npm or sdk-source.");
|
||||
}
|
||||
|
||||
var config = JSON.parse(fs.readFileSync(`${__dirname}/sdk-path-config.json`, 'utf8'));
|
||||
if (!config || !config.sdkPath) {
|
||||
printError("Must provide a valid sdk-path-config.json with a valid sdkPath.");
|
||||
printError("Must provide a valid sdk-path-config.json with a valid sdkPath.");
|
||||
}
|
||||
else {
|
||||
config.sdkPath = path.resolve(__dirname, config.sdkPath);
|
||||
config.sdkPath = path.resolve(__dirname, config.sdkPath);
|
||||
}
|
||||
|
||||
forEachSample((sampleDir) => {
|
||||
updateSdkTarget(sampleDir, config.sdkPath);
|
||||
updateSdkTarget(sampleDir, config.sdkPath);
|
||||
});
|
||||
|
||||
process.exit();
|
||||
|
|
|
@ -5,12 +5,12 @@ const scriptPath = __dirname;
|
|||
const samplesPath = path.join(scriptPath, '..', 'samples');
|
||||
|
||||
function forEachSample(cb) {
|
||||
fs.readdirSync(samplesPath).forEach((entry) => {
|
||||
const sampleDir = path.join(samplesPath, entry);
|
||||
if (fs.statSync(sampleDir).isDirectory()) {
|
||||
cb(sampleDir);
|
||||
}
|
||||
});
|
||||
fs.readdirSync(samplesPath).forEach((entry) => {
|
||||
const sampleDir = path.join(samplesPath, entry);
|
||||
if (fs.statSync(sampleDir).isDirectory()) {
|
||||
cb(sampleDir);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = forEachSample;
|
||||
|
|
|
@ -2,7 +2,7 @@ var shell = require('shelljs');
|
|||
var forEachSample = require('./foreach-sample');
|
||||
|
||||
forEachSample((sampleDir) => {
|
||||
var cmd = `pushd ${sampleDir} && npm install && popd`;
|
||||
console.log(cmd);
|
||||
shell.exec(cmd);
|
||||
var cmd = `pushd ${sampleDir} && npm install && popd`;
|
||||
console.log(cmd);
|
||||
shell.exec(cmd);
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"sdkPath": "../../mixed-reality-extension-sdk/packages/sdk"
|
||||
"sdkPath": "../../mixed-reality-extension-sdk/packages/sdk"
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче