mirror of
https://github.com/JannisX11/blockbench.git
synced 2025-02-11 16:12:06 +08:00
Merge branch 'validator' into next
This commit is contained in:
commit
ce9046acd4
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
17
js/api.js
17
js/api.js
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -329,6 +329,7 @@ var codec = new Codec('project', {
|
||||
}
|
||||
Canvas.updateAllBones()
|
||||
Canvas.updateAllPositions()
|
||||
Validator.validate()
|
||||
this.dispatchEvent('parsed', {model})
|
||||
},
|
||||
merge(model, path) {
|
||||
|
@ -728,6 +728,7 @@ function calculateVisibleBox() {
|
||||
if (Format.id == 'bedrock') Project.BedrockEntityManager.initEntity();
|
||||
if (Format.id == 'bedrock_block') Project.BedrockBlockManager.initBlock();
|
||||
}
|
||||
Validator.validate()
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
|
@ -136,6 +136,7 @@ function parseGeometry(data) {
|
||||
if (isApp && Project.geometry_name) {
|
||||
Project.BedrockEntityManager.initEntity()
|
||||
}
|
||||
Validator.validate()
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
|
@ -447,6 +447,7 @@ var codec = new Codec('java_block', {
|
||||
if (add) {
|
||||
Undo.finishEdit('Add block model')
|
||||
}
|
||||
Validator.validate()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -296,6 +296,7 @@ var codec = new Codec('optifine_entity', {
|
||||
importTexture(model.texture);
|
||||
this.dispatchEvent('parsed', {model});
|
||||
Canvas.updateAllBones();
|
||||
Validator.validate()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -209,6 +209,7 @@ var part_codec = new Codec('optifine_part', {
|
||||
addSubmodel(model)
|
||||
this.dispatchEvent('parsed', {model});
|
||||
Canvas.updateAllBones()
|
||||
Validator.validate()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -225,6 +225,7 @@ class ModelProject {
|
||||
setStartScreen(!Project);
|
||||
updateInterface();
|
||||
updateProjectResolution();
|
||||
Validator.validate();
|
||||
Vue.nextTick(() => {
|
||||
loadTextureDraggable();
|
||||
|
||||
|
@ -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
184
js/validator.js
Normal 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.`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user