зеркало из https://github.com/mozilla/Spoke.git
Merge pull request #1095 from mozilla/qa-test
Hubs Cloud Update 2021-02-02
This commit is contained in:
Коммит
4a17cb0c62
|
@ -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. We’ll 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. We’ll 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"
|
||||
];
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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
Двоичные данные
test/integration/snapshots/Editor.test.js.snap
Двоичный файл не отображается.
Загрузка…
Ссылка в новой задаче