Merge branch 'validator' into next

This commit is contained in:
JannisX11 2022-08-09 23:14:28 +02:00
commit ce9046acd4
17 changed files with 255 additions and 6 deletions

View File

@ -1358,6 +1358,31 @@
left: 49px;
}
/* Validator */
li.validator_dialog_problem {
background-color: var(--color-back);
padding: 3px 5px;
margin-bottom: 5px;
display: flex;
}
li.validator_dialog_problem.validator_warning > i {
color: var(--color-warning);
margin: 3px 4px;
}
li.validator_dialog_problem.validator_error > i {
color: var(--color-error);
margin: 3px 4px;
}
li.validator_dialog_problem span {
flex-grow: 1;
flex-shrink: 1;
padding-top: 3px;
}
li.validator_dialog_problem .tool {
flex-shrink: 0;
flex-grow: 0;
}
/* Edit History */
#edit_history_list ul {
margin-left: 14px;

View File

@ -317,7 +317,10 @@
--color-close: #d62e3f;
--color-confirm: #90ee90;
--color-error: #ff2a51;
--color-warning: #ffc400;
--color-stream: #6442A4;
--color-axis-x: #ff1242;
--color-axis-y: #23d400;
--color-axis-z: #0894ed;

View File

@ -699,6 +699,15 @@
#status_bar .status_selection_info {
color: var(--color-subtle_text);
}
#status_bar #validator_status {
cursor: pointer;
}
#status_bar #validator_status i {
vertical-align: sub;
margin-left: 2px;
margin-right: 2px;
font-size: 20px;
}
#status_bar .sidebar_toggle_button {
cursor: pointer;
padding: 2px;

View File

@ -133,6 +133,7 @@
<script src="js/animations/keyframe.js"></script>
<script src="js/animations/timeline.js"></script>
<script src="js/preview/preview_scenes.js"></script>
<script src="js/validator.js"></script>
<script src="js/plugin_loader.js"></script>
<script src="js/io/codec.js"></script>

View File

@ -366,14 +366,19 @@ const Blockbench = {
//Events
dispatchEvent(event_name, data) {
let list = this.events[event_name];
if (!list) return;
let results = [];
for (let i = 0; i < list.length; i++) {
if (typeof list[i] === 'function') {
let result = list[i](data);
results.push(result);
let results;
if (list) {
results = [];
for (let i = 0; i < list.length; i++) {
if (typeof list[i] === 'function') {
let result = list[i](data);
results.push(result);
}
}
}
if (Validator.triggers.includes(event_name)) {
Validator.validate(event_name);
}
return results;
},
addListener(event_names, cb) {

View File

@ -689,6 +689,8 @@ onVueSetup(function() {
selection_info: '',
Format: null,
show_modifier_keys: settings.status_bar_modifier_keys.value,
warnings: Validator.warnings,
errors: Validator.errors,
modifier_keys: {
ctrl: [],
shift: [],
@ -745,6 +747,9 @@ onVueSetup(function() {
clickModifiers() {
ActionControl.select(`setting: ${tl('settings.status_bar_modifier_keys')}`);
},
openValidator() {
Validator.openDialog();
},
toggleSidebar: Interface.toggleSidebar,
getIconNode: Blockbench.getIconNode,
tl
@ -787,6 +792,12 @@ onVueSetup(function() {
</template>
<div class="status_selection_info">{{ selection_info }}</div>
<div class="f_right" id="validator_status" v-if="warnings.length || errors.length" @click="openValidator()">
<span v-if="warnings.length" style="color: var(--color-warning)">{{ warnings.length }}<i class="material-icons">warning</i></span>
<span v-if="errors.length" style="color: var(--color-error)">{{ errors.length }}<i class="material-icons">error</i></span>
</div>
<div class="f_right">
{{ Prop.fps }} FPS
</div>

View File

@ -329,6 +329,7 @@ var codec = new Codec('project', {
}
Canvas.updateAllBones()
Canvas.updateAllPositions()
Validator.validate()
this.dispatchEvent('parsed', {model})
},
merge(model, path) {

View File

@ -728,6 +728,7 @@ function calculateVisibleBox() {
if (Format.id == 'bedrock') Project.BedrockEntityManager.initEntity();
if (Format.id == 'bedrock_block') Project.BedrockBlockManager.initBlock();
}
Validator.validate()
updateSelection()
}

View File

@ -136,6 +136,7 @@ function parseGeometry(data) {
if (isApp && Project.geometry_name) {
Project.BedrockEntityManager.initEntity()
}
Validator.validate()
updateSelection()
}

View File

@ -447,6 +447,7 @@ var codec = new Codec('java_block', {
if (add) {
Undo.finishEdit('Add block model')
}
Validator.validate()
},
})

View File

@ -829,6 +829,7 @@ var codec = new Codec('modded_entity', {
Project.geometry_name = geo_name;
this.dispatchEvent('parsed', {model});
Canvas.updateAllBones();
Validator.validate()
},
afterDownload(path) {
if (this.remember) {

View File

@ -296,6 +296,7 @@ var codec = new Codec('optifine_entity', {
importTexture(model.texture);
this.dispatchEvent('parsed', {model});
Canvas.updateAllBones();
Validator.validate()
}
})

View File

@ -209,6 +209,7 @@ var part_codec = new Codec('optifine_part', {
addSubmodel(model)
this.dispatchEvent('parsed', {model});
Canvas.updateAllBones()
Validator.validate()
}
})

View File

@ -225,6 +225,7 @@ class ModelProject {
setStartScreen(!Project);
updateInterface();
updateProjectResolution();
Validator.validate();
Vue.nextTick(() => {
loadTextureDraggable();

View File

@ -249,6 +249,7 @@ function unselectAll() {
const AutoBackupModels = {};
setInterval(function() {
if (Project && (Outliner.root.length || Project.textures.length)) {
Validator.validate();
try {
var model = Codecs.project.compile({compressed: false, backup: true, raw: true});
AutoBackupModels[Project.uuid] = model;

184
js/validator.js Normal file
View File

@ -0,0 +1,184 @@
const Validator = {
checks: [],
warnings: [],
errors: [],
validate(trigger) {
Validator.warnings.empty();
Validator.errors.empty();
if (!Project) return;
Validator.checks.forEach(check => {
try {
if (!Condition(check.condition)) return;
if (!trigger || check.update_triggers.includes(trigger)) {
check.update();
}
Validator.warnings.push(...check.warnings);
Validator.errors.push(...check.errors);
} catch (error) {
console.error(error);
}
})
},
openDialog() {
if (!Validator.dialog) {
Validator.dialog = new Dialog({
id: 'validator',
title: 'action.validator_window',
singleButton: true,
component: {
data() {return {
warnings: Validator.warnings,
errors: Validator.errors,
}},
computed: {
problems() {
this.errors.forEach(error => error.error = true);
return [...this.errors, ...this.warnings]
}
},
methods: {
getIconNode: Blockbench.getIconNode
},
template: `
<template>
<ul>
<li v-for="problem in problems" class="validator_dialog_problem" :class="problem.error ? 'validator_error' : 'validator_warning'" :key="problem.message">
<i class="material-icons">{{ problem.error ? 'error' : 'warning' }}</i>
<span>{{ problem.message }}</span>
<template v-if="problem.buttons">
<div v-for="button in problem.buttons" class="tool" :title="button.name" @click="button.click($event)">
<div class="icon_wrapper plugin_icon normal" v-html="getIconNode(button.icon, button.color).outerHTML"></div>
</div>
</div>
</li>
</ul>
</template>
`
}
});
}
Validator.dialog.show();
},
triggers: [],
updateCashedTriggers() {
Validator.triggers.empty();
Validator.checks.forEach(check => {
Validator.triggers.safePush(...check.update_triggers);
})
}
};
class ValidatorCheck {
constructor(id, options) {
this.id = id;
this.type = options.type;
this.update_triggers = options.update_triggers
? (options.update_triggers instanceof Array ? options.update_triggers : [options.update_triggers])
: [];
this.condition = options.condition;
this.run = options.run;
this.errors = [];
this.warnings = [];
Validator.checks.push(this);
Validator.updateCashedTriggers();
}
delete() {
Validator.checks.remove(this);
Validator.updateCashedTriggers();
}
update() {
this.errors.empty();
this.warnings.empty();
try {
this.run();
if (this.errors.length > 100) this.errors.splice(100);
if (this.warnings.length > 100) this.warnings.splice(100);
} catch (error) {
console.error(error);
}
}
warn(...warnings) {
this.warnings.push(...warnings);
}
fail(...errors) {
this.errors.push(...errors);
}
}
BARS.defineActions(function() {
new Action('validator_window', {
icon: 'checklist',
category: 'file',
condition: () => Project,
click() {
Validator.openDialog();
}
})
})
new ValidatorCheck('texture_names', {
condition: {formats: ['java_block']},
update_triggers: ['add_texture', 'change_texture_path'],
run() {
Texture.all.forEach(texture => {
let characters = (texture.folder + texture.name).match(/[^a-z0-9._/\\-]/)
if (characters) {
this.warn({
message: `Texture "${texture.name}" contains the following invalid characters: "${characters.join('')}"`,
buttons: [
{
name: 'Select Texture',
icon: 'mouse',
click() {
Validator.dialog.hide();
texture.select();
}
}
]
})
}
})
}
})
new ValidatorCheck('catmullrom_keyframes', {
condition: {features: ['animation_files']},
update_triggers: ['update_keyframe_selection'],
run() {
Animation.all.forEach(animation => {
for (let key in animation.animators) {
let animator = animation.animators[key];
if (animator instanceof BoneAnimator) {
for (let channel in animator.channels) {
if (!animator[channel] || !animator[channel].find(kf => kf.interpolation == 'catmullrom')) continue;
let keyframes = animator[channel].slice().sort((a, b) => a.time - b.time);
keyframes.forEach((kf, i) => {
if (kf.interpolation == 'catmullrom') {
if (kf.data_points.find(dp => isNaN(dp.x) || isNaN(dp.y) || isNaN(dp.z))) {
this.fail({
message: `${animator.channels[channel].name} keyframe at ${kf.time.toFixed(2)} on "${animator.name}" in "${animation.name}" contains non-numeric value. Smooth keyframes cannot contain math expressions.`
})
}
if ((!keyframes[i-1] || keyframes[i-1].interpolation != 'catmullrom') && (!keyframes[i+1] || keyframes[i+1].interpolation != 'catmullrom')) {
this.warn({
message: `${animator.channels[channel].name} keyframe at ${kf.time.toFixed(2)} on "${animator.name}" in "${animation.name}" is not surrounted by smooth keyframes. Multiple smooth keyframes are required to create a smooth spline.`
})
}
}
})
}
}
}
})
}
})

View File

@ -973,6 +973,8 @@
"action.close_project.desc": "Closes the currently open project",
"action.switch_tabs": "Switch Tabs",
"action.switch_tabs.desc": "Cycle between opened tabs. Hold shift to cycle in the opposite direction.",
"action.validator_window": "View Model Issues...",
"action.validator_window.desc": "Opens the validator, which lists issues with the current model.",
"action.import_obj": "Import OBJ Model",
"action.import_obj.desc": "Imports objects from an OBJ model as meshes",
"action.import_java_block_model": "Add Java Block/Item Model",