Merge pull request #1095 from mozilla/qa-test

Hubs Cloud Update 2021-02-02
This commit is contained in:
Brian Peiris 2021-02-02 22:46:12 -05:00 коммит произвёл GitHub
Родитель d185cf5e0b e37746b9fc
Коммит 4a17cb0c62
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 1611 добавлений и 128 удалений

105
Jenkinsfile поставляемый
Просмотреть файл

@ -1,19 +1,5 @@
import groovy.json.JsonOutput
// From https://issues.jenkins-ci.org/browse/JENKINS-44231
// Given arbitrary string returns a strongly escaped shell string literal.
// I.e. it will be in single quotes which turns off interpolation of $(...), etc.
// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command).
def shellString(s) {
// Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'.
// 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way.
// 2) And why not use ANSI-C quoting? I.e. we could replace ' with \'
// and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573).
// Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873).
'\'' + s.replace('\'', '\'\\\'\'') + '\''
}
pipeline {
agent any
@ -43,14 +29,37 @@ pipeline {
def slackURL = env.SLACK_URL
def sentryDsn = env.SENTRY_DSN
def gaTrackingId = env.GA_TRACKING_ID
def buildNumber = env.BUILD_NUMBER
def gitCommit = env.GIT_COMMIT
def jobName = env.JOB_NAME
def disableDeploy = env.DISABLE_DEPLOY
def showQAPromoteCommand = env.SHOW_QA_PROMOTE_COMMAND
def qaBuildsSlackChannel = env.QA_BUILDS_SLACK_CHANNEL
def habCommand = "/bin/bash scripts/hab-build-and-push.sh \\\"${baseAssetsPath}\\\" \\\"${hubsServer}\\\" \\\"${reticulumServer}\\\" \\\"${thumbnailServer}\\\" \\\"${corsProxyServer}\\\" \\\"${nonCorsProxyDomains}\\\" \\\"${sentryDsn}\\\" \\\"${gaTrackingId}\\\" \\\"${targetS3Bucket}\\\" \\\"${isMoz}\\\" \\\"${env.BUILD_NUMBER}\\\" \\\"${env.GIT_COMMIT}\\\""
sh "/usr/bin/script --return -c ${shellString(habCommand)} /dev/null"
def habCommand = (
"/bin/bash scripts/hab-build-and-push.sh "
+ "\\\"${baseAssetsPath}\\\" "
+ "\\\"${hubsServer}\\\" "
+ "\\\"${reticulumServer}\\\" "
+ "\\\"${thumbnailServer}\\\" "
+ "\\\"${corsProxyServer}\\\" "
+ "\\\"${nonCorsProxyDomains}\\\" "
+ "\\\"${sentryDsn}\\\" "
+ "\\\"${gaTrackingId}\\\" "
+ "\\\"${targetS3Bucket}\\\" "
+ "\\\"${isMoz}\\\" "
+ "\\\"${buildNumber}\\\" "
+ "\\\"${gitCommit}\\\" "
+ "\\\"${disableDeploy}\\\" "
)
runCommand(habCommand)
def s = $/eval 'ls -rt results/*.hart | head -n 1'/$
def s = $/eval 'ls -t results/*.hart | head -n 1'/$
def hart = sh(returnStdout: true, script: "${s}").trim()
s = $/eval 'tail -n +6 ${hart} | xzcat | tar tf - | grep IDENT'/$
def identPath = sh(returnStdout: true, script: "${s}").trim()
s = $/eval 'tail -n +6 ${hart} | xzcat | tar xf - "${identPath}" -O'/$
def packageIdent = sh(returnStdout: true, script: "${s}").trim()
def packageTimeVersion = packageIdent.tokenize('/')[3]
@ -59,22 +68,56 @@ pipeline {
def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim()
def gitSha = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
def text = (
"*<http://localhost:8080/job/${env.JOB_NAME}/${env.BUILD_NUMBER}|#${env.BUILD_NUMBER}>* *${env.JOB_NAME}* " +
"<https://github.com/mozilla/Spoke/commit/$gitSha|$gitSha> ${spokeVersion} " +
"Spoke: ```${gitSha} ${gitMessage}```\n" +
"<${smokeURL}?required_version=${spokeVersion}|Smoke Test> - to push:\n" +
"`/mr spoke deploy ${spokeVersion} s3://${targetS3Bucket}`"
)
def payload = 'payload=' + JsonOutput.toJson([
text : text,
channel : "#mr-builds",
username : "buildbot",
icon_emoji: ":gift:"
])
sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}"
if (showQAPromoteCommand == "true") {
def text = (
"*<http://localhost:8080/job/${jobName}/${buildNumber}|#${buildNumber}>* *${jobName}* " +
"<https://github.com/mozilla/Spoke/commit/$gitSha|$gitSha> ${spokeVersion} " +
"Spoke: ```${gitSha} ${gitMessage}```\n" +
"${packageIdent} built and uploaded - to promote:\n" +
"`/mr promote-spoke-qa ${packageIdent}`"
)
sendSlackMessage(text, qaBuildsSlackChannel, ":gift:", slackURL)
} else {
def text = (
"*<http://localhost:8080/job/${jobName}/${buildNumber}|#${buildNumber}>* *${jobName}* " +
"<https://github.com/mozilla/Spoke/commit/$gitSha|$gitSha> ${spokeVersion} " +
"Spoke: ```${gitSha} ${gitMessage}```\n" +
"<${smokeURL}?required_version=${spokeVersion}|Smoke Test> - to push:\n" +
"`/mr spoke deploy ${spokeVersion} s3://${targetS3Bucket}`"
)
sendSlackMessage(text, "#mr-builds", ":gift:", slackURL)
}
}
}
}
}
}
def runCommand(command) {
sh "/usr/bin/script --return -c ${shellString(command)} /dev/null"
}
def sendSlackMessage(text, channel, icon, slackURL) {
def payload = 'payload=' + JsonOutput.toJson([
text : text,
channel : channel,
username : "buildbot",
icon_emoji: icon
])
sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}"
}
// From https://issues.jenkins-ci.org/browse/JENKINS-44231
// Given arbitrary string returns a strongly escaped shell string literal.
// I.e. it will be in single quotes which turns off interpolation of $(...), etc.
// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command).
def shellString(s) {
// Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'.
// 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way.
// 2) And why not use ANSI-C quoting? I.e. we could replace ' with \'
// and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573).
// Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873).
'\'' + s.replace('\'', '\'\\\'\'') + '\''
}

Просмотреть файл

@ -1,6 +1,6 @@
## Remixing
Allowing remixing means that others can download, edit, change, and re-publish your Spoke scene as long as they give credit to the creators of the scene. If you allow allow remixing, you agree to license your scene under a [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/legalcode) license. Well provide you an opportunity to include the attribution information you want to include. This information will be associated with the scene, but not stored with you account.
Allowing remixing means that others can download, edit, change, and re-publish your Spoke scene as long as they give credit to the creators of the scene. If you allow allow remixing, you agree to license your scene under a [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/legalcode) license. Well provide you an opportunity to include the attribution information you want to include. This information will be associated with the scene, but not stored with your account.
You can change this setting at any time, by re-publishing your scene.

Просмотреть файл

@ -31,7 +31,7 @@
"test": "concurrently --success \"first\" --kill-others \"yarn test-all\" \"yarn test-server\"",
"test-server": "cross-env NODE_ENV=test webpack-dev-server --mode development --content-base ./test/fixtures",
"test-all": "ava --verbose",
"test-ci": "ava --tap | tap-xunit > ./test/reports/ava.xml",
"test-ci": "ava --serial --tap | tap-xunit > ./test/reports/ava.xml",
"unit-tests": "ava ./test/unit/**/*.test.js",
"integration-tests": "concurrently --success \"first\" --kill-others \"ava ./test/integration\" \"yarn test-server\"",
"update-test-snapshots": "concurrently --success \"first\" --kill-others \"ava -u\" \"yarn test-server\"",

Просмотреть файл

@ -12,6 +12,7 @@ export TARGET_S3_BUCKET=$9
export IS_MOZ=${10}
export BUILD_NUMBER=${11}
export GIT_COMMIT=${12}
export DISABLE_DEPLOY=${13}
export BUILD_VERSION="${BUILD_NUMBER} (${GIT_COMMIT})"
export SENTRY_LOG_LEVEL=debug
@ -38,6 +39,11 @@ sudo /usr/bin/hab-pkg-install results/*.hart
hab svc load $PKG
hab svc stop $PKG
DEPLOY_TYPE="s3"
if [[ DISABLE_DEPLOY == "true" ]]; then
DEPLOY_TYPE="none"
fi
# Apparently these vars come in from jenkins with quotes already
cat > build-config.toml << EOTOML
[general]
@ -52,7 +58,7 @@ ga_tracking_id = $GA_TRACKING_ID
is_moz = $IS_MOZ
[deploy]
type = "s3"
type = "$DEPLOY_TYPE"
target = $TARGET_S3_BUCKET
region = "us-west-1"
EOTOML

Просмотреть файл

@ -131,7 +131,7 @@ export default class MeshCombinationGroup {
}
await asyncTraverse(rootObject, async object => {
if (isStatic(object) && object.isMesh) {
if (isStatic(object) && object.isMesh && object._combine !== false) {
let added = false;
for (const group of meshCombinationGroups) {

Просмотреть файл

@ -208,6 +208,13 @@ export default function EditorNodeMixin(Object3DClass) {
}
}
/**
* Adds a GLTF component
* @param {String} name The component name to be replaced
* @param {Object} props The component properties to be set
* @param {boolean} params Parameters object
* @param {{THREE.Object3D} params.node The target Object3D element
*/
addGLTFComponent(name, props) {
if (!this.userData.gltfExtensions) {
this.userData.gltfExtensions = {};

Просмотреть файл

@ -3,6 +3,7 @@ import Model from "../objects/Model";
import { PropertyBinding } from "three";
import { setStaticMode, StaticModes } from "../StaticMode";
import cloneObject3D from "../utils/cloneObject3D";
import { getComponents } from "../gltf/moz-hubs-components";
import { isKitPieceNode, getComponent, getGLTFComponent, traverseGltfNode } from "../gltf/moz-hubs-components";
import { RethrownError } from "../utils/errors";
@ -28,17 +29,22 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
node.collidable = !!json.components.find(c => c.name === "collidable");
node.walkable = !!json.components.find(c => c.name === "walkable");
node.combine = !!json.components.find(c => c.name === "combine");
const loopAnimationComponent = json.components.find(c => c.name === "loop-animation");
if (loopAnimationComponent && loopAnimationComponent.props) {
const { clip, activeClipIndex } = loopAnimationComponent.props;
const { clip, activeClipIndices } = loopAnimationComponent.props;
if (activeClipIndex !== undefined) {
node.activeClipIndex = loopAnimationComponent.props.activeClipIndex;
} else if (clip !== undefined && node.model && node.model.animations) {
if (clip !== undefined && node.model && node.model.animations) {
// DEPRECATED: Old loop-animation component stored the clip name rather than the clip index
node.activeClipIndex = node.model.animations.findIndex(animation => animation.name === clip);
const clipIndex = node.model.animations.findIndex(animation => animation.name === clip);
if (clipIndex !== -1) {
node.activeClipItems = node.getActiveItems([clipIndex]);
}
} else {
node.activeClipItems = node.getActiveItems(activeClipIndices);
}
}
@ -60,6 +66,7 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
this._canonicalUrl = "";
this.collidable = true;
this.walkable = true;
this.combine = true;
this._kitId = null;
this._pieceId = null;
this.subPieces = [];
@ -422,9 +429,9 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
}
};
if (this.activeClipIndex !== -1) {
if (this.activeClipIndices.length > 0) {
components["loop-animation"] = {
activeClipIndex: this.activeClipIndex
activeClipIndices: this.activeClipIndices
};
}
@ -436,6 +443,10 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
components.walkable = {};
}
if (this.combine) {
components.combine = {};
}
return super.serialize(components);
}
@ -448,6 +459,7 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
this._pieceId = source._pieceId;
this.collidable = source.collidable;
this.walkable = source.walkable;
this.combine = source.combine;
// TODO update the sub-piece copy method
if (this.model) {
@ -502,19 +514,22 @@ export default class KitPieceNode extends EditorNodeMixin(Model) {
receive: this.receiveShadow
});
// TODO: Support exporting more than one active clip.
if (this.activeClip) {
const activeClipIndex = ctx.animations.indexOf(this.activeClip);
const clipIndices = this.activeClipIndices.map(index => {
return ctx.animations.indexOf(this.model.animations[index]);
});
if (activeClipIndex === -1) {
throw new Error(
`Error exporting model "${this.name}" with url "${this._canonicalUrl}". Animation could not be found.`
);
} else {
this.addGLTFComponent("loop-animation", {
activeClipIndex: activeClipIndex
});
this.model.traverse(child => {
const components = getComponents(child);
if (components && components["loop-animation"]) {
delete components["loop-animation"];
}
});
if (clipIndices.length > 0) {
this.addGLTFComponent("loop-animation", {
activeClipIndices: clipIndices
});
}
if (this.model) {

Просмотреть файл

@ -3,6 +3,7 @@ import Model from "../objects/Model";
import EditorNodeMixin from "./EditorNodeMixin";
import { setStaticMode, StaticModes } from "../StaticMode";
import cloneObject3D from "../utils/cloneObject3D";
import { getComponents } from "../gltf/moz-hubs-components";
import { RethrownError } from "../utils/errors";
import { getObjectPerfIssues, maybeAddLargeFileIssue } from "../utils/performance";
@ -48,17 +49,22 @@ export default class ModelNode extends EditorNodeMixin(Model) {
node.collidable = !!json.components.find(c => c.name === "collidable");
node.walkable = !!json.components.find(c => c.name === "walkable");
node.combine = !!json.components.find(c => c.name === "combine");
const loopAnimationComponent = json.components.find(c => c.name === "loop-animation");
if (loopAnimationComponent && loopAnimationComponent.props) {
const { clip, activeClipIndex } = loopAnimationComponent.props;
const { clip, activeClipIndices } = loopAnimationComponent.props;
if (activeClipIndex !== undefined) {
node.activeClipIndex = loopAnimationComponent.props.activeClipIndex;
} else if (clip !== undefined && node.model && node.model.animations) {
if (clip !== undefined && node.model && node.model.animations) {
// DEPRECATED: Old loop-animation component stored the clip name rather than the clip index
node.activeClipIndex = node.model.animations.findIndex(animation => animation.name === clip);
const clipIndex = node.model.animations.findIndex(animation => animation.name === clip);
if (clipIndex !== -1) {
node.activeClipItems = node.getActiveItems([clipIndex]);
}
} else {
node.activeClipItems = node.getActiveItems(activeClipIndices);
}
}
@ -80,6 +86,7 @@ export default class ModelNode extends EditorNodeMixin(Model) {
this._canonicalUrl = "";
this.collidable = true;
this.walkable = true;
this.combine = true;
this.initialScale = 1;
this.boundingBox = new Box3();
this.boundingSphere = new Sphere();
@ -315,9 +322,9 @@ export default class ModelNode extends EditorNodeMixin(Model) {
}
};
if (this.activeClipIndex !== -1) {
if (this.activeClipIndices.length > 0) {
components["loop-animation"] = {
activeClipIndex: this.activeClipIndex
activeClipIndices: this.activeClipIndices
};
}
@ -329,6 +336,10 @@ export default class ModelNode extends EditorNodeMixin(Model) {
components.walkable = {};
}
if (this.combine) {
components.combine = {};
}
return super.serialize(components);
}
@ -348,6 +359,7 @@ export default class ModelNode extends EditorNodeMixin(Model) {
this.attribution = source.attribution;
this.collidable = source.collidable;
this.walkable = source.walkable;
this.combine = source.combine;
return this;
}
@ -358,19 +370,22 @@ export default class ModelNode extends EditorNodeMixin(Model) {
receive: this.receiveShadow
});
// TODO: Support exporting more than one active clip.
if (this.activeClip) {
const activeClipIndex = ctx.animations.indexOf(this.activeClip);
const clipIndices = this.activeClipIndices.map(index => {
return ctx.animations.indexOf(this.model.animations[index]);
});
if (activeClipIndex === -1) {
throw new Error(
`Error exporting model "${this.name}" with url "${this._canonicalUrl}". Animation could not be found.`
);
} else {
this.addGLTFComponent("loop-animation", {
activeClipIndex: activeClipIndex
});
this.model.traverse(child => {
const components = getComponents(child);
if (components && components["loop-animation"]) {
delete components["loop-animation"];
}
});
if (clipIndices.length > 0) {
this.addGLTFComponent("loop-animation", {
activeClipIndices: clipIndices
});
}
}
}

Просмотреть файл

@ -138,6 +138,44 @@ function migrateV3ToV4(json) {
return json;
}
const combineComponents = ["gltf-model", "kit-piece"];
function migrateV4ToV5(json) {
json.version = 5;
for (const entityId in json.entities) {
if (!Object.prototype.hasOwnProperty.call(json.entities, entityId)) continue;
const entity = json.entities[entityId];
if (!entity.components) {
continue;
}
const animationComponent = entity.components.find(c => c.name === "loop-animation");
if (animationComponent) {
// Prior to V5 animation clips were stored in activeClipIndex as an integer
const { activeClipIndex } = animationComponent.props;
delete animationComponent.props.activeClipIndex;
// In V5+ activeClipIndices stores an array of integers. It may be undefined if migrating from a legacy scene where the
// clip property stores the animation clip name. We can't migrate this here so we do it in ModelNode and KitPieceNode.
animationComponent.props.activeClipIndices = activeClipIndex !== undefined ? [activeClipIndex] : [];
}
const hasCombineComponent = entity.components.find(c => combineComponents.indexOf(c.name) !== -1);
if (hasCombineComponent) {
entity.components.push({
name: "combine",
props: {}
});
}
}
return json;
}
export const FogType = {
Disabled: "disabled",
Linear: "linear",
@ -166,6 +204,10 @@ export default class SceneNode extends EditorNodeMixin(Scene) {
json = migrateV3ToV4(json);
}
if (json.version === 4) {
json = migrateV4ToV5(json);
}
const { root, metadata, entities } = json;
let scene = null;
@ -403,7 +445,7 @@ export default class SceneNode extends EditorNodeMixin(Scene) {
serialize() {
const sceneJson = {
version: 4,
version: 5,
root: this.uuid,
metadata: JSON.parse(JSON.stringify(this.metadata)),
entities: {
@ -577,11 +619,7 @@ export default class SceneNode extends EditorNodeMixin(Scene) {
this.traverse(child => {
if (child.isNode && child.type === "Model") {
const activeClip = child.activeClip;
if (activeClip) {
animations.push(child.activeClip);
}
animations.push(...child.clips);
}
});

Просмотреть файл

@ -32,7 +32,9 @@ export default class SpawnerNode extends EditorNodeMixin(Model) {
static async deserialize(editor, json, loadAsync, onError) {
const node = await super.deserialize(editor, json);
const { src } = json.components.find(c => c.name === "spawner").props;
const { src, applyGravity } = json.components.find(c => c.name === "spawner").props;
node.applyGravity = !!applyGravity;
loadAsync(node.load(src, onError));
@ -47,6 +49,7 @@ export default class SpawnerNode extends EditorNodeMixin(Model) {
this.boundingSphere = new Sphere();
this.stats = defaultStats;
this.gltfJson = null;
this.applyGravity = false;
}
// Overrides Model's src property and stores the original (non-resolved) url.
@ -215,7 +218,8 @@ export default class SpawnerNode extends EditorNodeMixin(Model) {
serialize() {
return super.serialize({
spawner: {
src: this._canonicalUrl
src: this._canonicalUrl,
applyGravity: this.applyGravity
}
});
}
@ -232,13 +236,18 @@ export default class SpawnerNode extends EditorNodeMixin(Model) {
this._canonicalUrl = source._canonicalUrl;
}
this.applyGravity = source.applyGravity;
return this;
}
prepareForExport() {
super.prepareForExport();
this.addGLTFComponent("spawner", {
src: this._canonicalUrl
src: this._canonicalUrl,
mediaOptions: {
applyGravity: this.applyGravity
}
});
this.replaceObject();
}

Просмотреть файл

@ -1,4 +1,4 @@
import { Object3D, AnimationMixer } from "three";
import { Object3D, AnimationMixer, LoopRepeat } from "three";
import { GLTFLoader } from "../gltf/GLTFLoader";
import cloneObject3D from "../utils/cloneObject3D";
@ -11,10 +11,10 @@ export default class Model extends Object3D {
this._src = null;
this._castShadow = false;
this._receiveShadow = false;
// Use index instead of references to AnimationClips to simplify animation cloning / track name remapping
this.activeClipIndex = -1;
this._combine = true;
this.activeClipItems = [];
this.animationMixer = null;
this.activeClipAction = null;
this.currentActions = [];
}
get src() {
@ -54,6 +54,7 @@ export default class Model extends Object3D {
this.castShadow = this._castShadow;
this.receiveShadow = this._receiveShadow;
this.combine = this._combine;
return this;
}
@ -63,29 +64,66 @@ export default class Model extends Object3D {
this.model && this.model.animations
? this.model.animations.map((clip, index) => ({ label: clip.name, value: index }))
: [];
clipOptions.unshift({ label: "None", value: -1 });
if (clipOptions.length == 0) {
clipOptions.unshift({ label: "None", value: -1 });
}
return clipOptions;
}
get activeClip() {
return (this.model && this.model.animations && this.model.animations[this.activeClipIndex]) || null;
getActiveItems(indices) {
if (this.model && this.model.animations) {
return indices
.filter(item => item >= 0 && this.model.animations[item])
.map(item => {
const clip = this.model.animations[item];
return { label: clip.name, value: item };
});
}
return [];
}
get activeClipIndices() {
const activeClipIndices = this.activeClips.map(clip => {
const index = this.model.animations.indexOf(clip);
if (index === -1) {
throw new Error(
`Error exporting model "${this.name}" with url "${this._canonicalUrl}". Animation could not be found.`
);
}
return index;
});
return activeClipIndices;
}
get activeClips() {
if (this.model && this.model.animations) {
return this.activeClipItems
.filter(item => item.value >= 0)
.map(item => this.model.animations.find(({ name }) => name === item.label));
}
return [];
}
get clips() {
return this.model.animations;
}
updateAnimationState() {
const clip = this.activeClip;
const playingClip = this.activeClipAction && this.activeClipAction.getClip();
if (this.model.animations.length === 0) {
return;
}
if (clip !== playingClip) {
if (this.activeClipAction) {
this.activeClipAction.stop();
}
const activeClips = this.activeClips;
if (this.animationMixer && clip) {
this.activeClipAction = this.animationMixer.clipAction(clip);
this.activeClipAction.play();
} else {
this.activeClipAction = null;
}
if (!activeClips) return;
this.currentActions.length = 0;
for (let i = 0; i < activeClips.length; i++) {
const action = this.animationMixer.clipAction(activeClips[i], this.model);
action.enabled = true;
action.setLoop(LoopRepeat, Infinity).play();
this.currentActions.push(action);
}
}
@ -94,10 +132,11 @@ export default class Model extends Object3D {
}
stopAnimation() {
if (this.activeClipAction) {
this.activeClipAction.stop();
this.activeClipAction = null;
for (let i = 0; i < this.currentActions.length; i++) {
this.currentActions[i].enabled = false;
this.currentActions[i].stop();
}
this.currentActions.length = 0;
}
update(dt) {
@ -180,6 +219,20 @@ export default class Model extends Object3D {
}
}
get combine() {
return this._combine;
}
set combine(value) {
this._combine = value;
if (this.model) {
this.model.traverse(child => {
child._combine = value;
});
}
}
// TODO: Add play/pause methods for previewing animations.
copy(source, recursive = true) {
@ -205,7 +258,7 @@ export default class Model extends Object3D {
}
this._src = source._src;
this.activeClipIndex = source.activeClipIndex;
this.activeClipItems = source.activeClipItems;
return this;
}

Просмотреть файл

@ -55,6 +55,18 @@ const staticStyle = {
singleValue: (base, { isDisabled }) => ({
...base,
color: isDisabled ? "grey" : "white"
}),
multiValue: (base, { isDisabled }) => ({
...base,
backgroundColor: isDisabled ? "grey" : "#006EFF"
}),
multiValueLabel: (base, { isDisabled }) => ({
...base,
color: isDisabled ? "black" : "white"
}),
multiValueRemove: (base, { isFocused }) => ({
...base,
color: isFocused ? "grey" : "white"
})
};
@ -69,16 +81,17 @@ export default function SelectInput({
creatable,
...rest
}) {
const selectedOption =
options.find(o => {
if (o === null) {
return o;
} else if (o.value && o.value.equals) {
return o.value.equals(value);
} else {
return o.value === value;
}
}) || null;
const selectedOption = Array.isArray(value)
? value
: options.find(o => {
if (o === null) {
return o;
} else if (o.value && o.value.equals) {
return o.value.equals(value);
} else {
return o.value === value;
}
}) || null;
const dynamicStyle = {
...staticStyle,
@ -99,7 +112,17 @@ export default function SelectInput({
components={{ IndicatorSeparator: () => null }}
placeholder={placeholder}
options={options}
onChange={option => onChange(option && option.value, option)}
onChange={option => {
if (Array.isArray(option)) {
onChange(
option.filter(item => {
return item.value >= 0;
})
);
} else {
onChange(option && option.value, option);
}
}}
isDisabled={disabled}
/>
);

Просмотреть файл

@ -102,8 +102,8 @@ export default class KitPieceNodeEditor extends Component {
this.props.editor.loadMaterialSlotSelected(subPiece.id, materialSlot.id, materialId);
};
onChangeAnimation = activeClipIndex => {
this.props.editor.setPropertySelected("activeClipIndex", activeClipIndex);
onChangeAnimation = activeClipItems => {
this.props.editor.setPropertySelected("activeClipItems", activeClipItems || []);
};
onChangeCollidable = collidable => {
@ -122,6 +122,10 @@ export default class KitPieceNodeEditor extends Component {
this.props.editor.setPropertySelected("receiveShadow", receiveShadow);
};
onChangeCombine = combine => {
this.props.editor.setPropertySelected("combine", combine);
};
isAnimationPropertyDisabled() {
const { multiEdit, editor, node } = this.props;
@ -173,8 +177,11 @@ export default class KitPieceNodeEditor extends Component {
<SelectInput
disabled={this.isAnimationPropertyDisabled()}
options={node.getClipOptions()}
value={node.activeClipIndex}
value={node.activeClipItems}
onChange={this.onChangeAnimation}
className="basic-multi-select"
classNamePrefix="select"
isMulti
/>
</InputGroup>
<InputGroup name="Collidable">
@ -189,6 +196,9 @@ export default class KitPieceNodeEditor extends Component {
<InputGroup name="Receive Shadow">
<BooleanInput value={node.receiveShadow} onChange={this.onChangeReceiveShadow} />
</InputGroup>
<InputGroup name="Combine">
<BooleanInput value={node.combine} onChange={this.onChangeCombine} />
</InputGroup>
</NodeEditor>
);
}

Просмотреть файл

@ -23,8 +23,8 @@ export default class ModelNodeEditor extends Component {
this.props.editor.setPropertiesSelected({ ...initialProps, src });
};
onChangeAnimation = activeClipIndex => {
this.props.editor.setPropertySelected("activeClipIndex", activeClipIndex);
onChangeAnimation = activeClipItems => {
this.props.editor.setPropertySelected("activeClipItems", activeClipItems || []);
};
onChangeCollidable = collidable => {
@ -43,6 +43,10 @@ export default class ModelNodeEditor extends Component {
this.props.editor.setPropertySelected("receiveShadow", receiveShadow);
};
onChangeCombine = combine => {
this.props.editor.setPropertySelected("combine", combine);
};
isAnimationPropertyDisabled() {
const { multiEdit, editor, node } = this.props;
@ -65,8 +69,11 @@ export default class ModelNodeEditor extends Component {
<SelectInput
disabled={this.isAnimationPropertyDisabled()}
options={node.getClipOptions()}
value={node.activeClipIndex}
value={node.activeClipItems}
onChange={this.onChangeAnimation}
className="basic-multi-select"
classNamePrefix="select"
isMulti
/>
</InputGroup>
<InputGroup name="Collidable">
@ -81,6 +88,9 @@ export default class ModelNodeEditor extends Component {
<InputGroup name="Receive Shadow">
<BooleanInput value={node.receiveShadow} onChange={this.onChangeReceiveShadow} />
</InputGroup>
<InputGroup name="Combine">
<BooleanInput value={node.combine} onChange={this.onChangeCombine} />
</InputGroup>
{node.model && <GLTFInfo node={node} />}
</NodeEditor>
);

Просмотреть файл

@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import NodeEditor from "./NodeEditor";
import InputGroup from "../inputs/InputGroup";
import ModelInput from "../inputs/ModelInput";
import BooleanInput from "../inputs/BooleanInput";
import { Magic } from "styled-icons/fa-solid/Magic";
import { GLTFInfo } from "../inputs/GLTFInfo";
@ -20,6 +21,10 @@ export default class SpawnerNodeEditor extends Component {
this.props.editor.setPropertiesSelected({ ...initialProps, src });
};
onChangeApplyGravity = applyGravity => {
this.props.editor.setPropertySelected("applyGravity", applyGravity);
};
render() {
const node = this.props.node;
@ -28,6 +33,9 @@ export default class SpawnerNodeEditor extends Component {
<InputGroup name="Model Url">
<ModelInput value={node.src} onChange={this.onChangeSrc} />
</InputGroup>
<InputGroup name="Apply gravity to spawned object">
<BooleanInput value={node.applyGravity} onChange={this.onChangeApplyGravity} />
</InputGroup>
{node.model && <GLTFInfo node={node} />}
</NodeEditor>
);

Просмотреть файл

@ -10,7 +10,7 @@ const messages = {
"waypoint.label-canBeOccupied": "Can be occupied",
"waypoint.label-canBeClicked": "Clickable",
"waypoint.label-willDisableMotion": "Disable Motion",
"waypoint.label-willDisableTeleport": "Disable Teleporting",
"waypoint.label-willDisableTeleporting": "Disable Teleporting",
"waypoint.label-snapToNavMesh": "Snap to floor plan",
"waypoint.label-willMaintainInitialOrientation": "Maintain initial orientation",
"waypoint.description-canBeSpawnPoint": "Avatars may be teleported to this waypoint when entering the scene",
@ -19,7 +19,7 @@ const messages = {
"waypoint.description-canBeClicked":
"This waypoint will be visible in pause mode and clicking on it will teleport you to it",
"waypoint.description-willDisableMotion": "Avatars will not be able to move after using this waypoint",
"waypoint.description-willDisableTeleport": "Avatars will not be able to teleport after using this waypoint",
"waypoint.description-willDisableTeleporting": "Avatars will not be able to teleport after using this waypoint",
"waypoint.description-snapToNavMesh":
"Avatars will move as close as they can to this waypoint but will not leave the ground",
"waypoint.description-willMaintainInitialOrientation":
@ -31,7 +31,7 @@ const propertyNames = [
"canBeOccupied",
"canBeClicked",
"willDisableMotion",
"willDisableTeleport",
"willDisableTeleporting",
"snapToNavMesh",
"willMaintainInitialOrientation"
];

1
test/fixtures/V5TestScene.spoke поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -141,7 +141,7 @@ test("Editor should load V1TestScene", withPage(`/projects/new?template=${v1Test
t.is(shadow2Props.cast, false);
t.is(shadow2Props.receive, false);
const loopAnimation2Props = model2Entity.components.find(c => c.name === "loop-animation").props;
t.is(loopAnimation2Props.activeClipIndex, 0);
t.deepEqual(loopAnimation2Props.activeClipIndices, [0]);
const groupNode1Entity = entities.find(e => e.name === "Group");
const groupNode1EntityIndex = entities.findIndex(e => e.name === "Group");
@ -260,3 +260,11 @@ test("Editor should load V4TestScene", withPage(`/projects/new?template=${v4Test
const serializedScene = await getSerializedScene(page, sceneHandle);
t.snapshot(serializedScene);
});
const v5TestSceneUrl = getFixtureUrl("V5TestScene.spoke");
test("Editor should load V5TestScene", withPage(`/projects/new?template=${v5TestSceneUrl}`), async (t, page) => {
const sceneHandle = await waitForProjectLoaded(page);
const serializedScene = await getSerializedScene(page, sceneHandle);
t.snapshot(serializedScene);
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Двоичные данные
test/integration/snapshots/Editor.test.js.snap

Двоичный файл не отображается.