diff --git a/index.html b/index.html
index 85c135e1c..62901012d 100644
--- a/index.html
+++ b/index.html
@@ -86,7 +86,6 @@
-
@@ -156,6 +155,7 @@
+
diff --git a/js/animations/fabrik.js b/js/animations/fabrik.js
new file mode 100644
index 000000000..8a7986553
--- /dev/null
+++ b/js/animations/fabrik.js
@@ -0,0 +1,79 @@
+function fabrikIter(bones, target, pole) {
+ let n = bones.length;
+ let bases = bones.slice(0, -1);
+
+ let distances = bases.map((bone, i) => {
+ return bone.distanceTo(bones[i + 1]);
+ });
+
+ let dist = bones[0].distanceTo(target);
+
+ polecalc: if (pole) {
+ let target_offset = target.clone().sub(bones[0]);
+ let target_dir = target_offset.normalize();
+
+ let tip_offset = bones[n - 1].clone().sub(bones[0]);
+ let tip_dir = tip_offset.normalize();
+
+ let tip_to_target_rotation = new THREE.Quaternion().setFromUnitVectors(tip_dir, target_dir);
+
+ let pole_offset = pole.clone().sub(bones[0]);
+ let pole_dir = pole_offset.projectOnPlane(target_dir).normalize();
+
+ if (pole_dir.length() == 0) {
+ break polecalc;
+ }
+
+ let normal = target_dir.cross(pole_dir).normalize();
+
+ if (normal.length() == 0) {
+ break polecalc;
+ }
+
+ bones.forEach(bone => {
+ let offset = bone.clone().sub(bones[0]);
+ offset.applyQuaternion(tip_to_target_rotation);
+
+ offset.projectOnPlane(normal);
+
+ offset.add(bones[0]);
+ bone.copy(offset);
+ });
+ }
+
+ if (dist > distances.reduce((partial, a) => partial + a, 0)) {
+ for (i = 0; i < n - 1; i++) {
+ let pos = bones[i];
+ let r = pos.distanceTo(target);
+ let lambda = distances[i] / r;
+ bones[i + 1] = pos.clone().multiplyScalar(1 - lambda).add(target.clone().multiplyScalar(lambda));
+ }
+ } else {
+ let b = bases[0];
+ let diff = target.distanceTo(bones[n - 1]);
+ const TOLERANCE = 0.001;
+ while (diff > TOLERANCE) {
+ bones[n - 1] = target;
+ for (i = n - 2; i >= 0; i--) {
+ let p = bones[i];
+ let p2 = bones[i + 1];
+ let r = p.distanceTo(p2);
+ let lambda = distances[i] / r;
+ bones[i] = p2.clone().multiplyScalar(1 - lambda).add(p.clone().multiplyScalar(lambda));
+ }
+
+ bones[0] = b;
+
+ for (i = 0; i < n - 1; i++) {
+ let p = bones[i];
+ let p2 = bones[i + 1];
+ let r = p.distanceTo(p2);
+ let lambda = distances[i] / r;
+ bones[i + 1] = p.clone().multiplyScalar(1 - lambda).add(p2.clone().multiplyScalar(lambda));
+ }
+
+ diff = target.distanceTo(bones[n - 1]);
+ }
+ }
+}
+
diff --git a/js/animations/timeline_animators.js b/js/animations/timeline_animators.js
index 2c265b155..b75f397ce 100644
--- a/js/animations/timeline_animators.js
+++ b/js/animations/timeline_animators.js
@@ -55,7 +55,7 @@ class GeneralAnimator {
if (typeof time !== 'number') time = Timeline.time;
var keyframes = [];
if (undo) {
- Undo.initEdit({keyframes})
+ Undo.initEdit({ keyframes })
}
var keyframe = new Keyframe({
channel: channel,
@@ -93,7 +93,7 @@ class GeneralAnimator {
}
getOrMakeKeyframe(channel) {
let before, result;
- let epsilon = Timeline.getStep()/2 || 0.01;
+ let epsilon = Timeline.getStep() / 2 || 0.01;
let has_before = false;
for (let kf of this[channel]) {
@@ -109,7 +109,7 @@ class GeneralAnimator {
if (settings.auto_keyframe.value && Timeline.snapTime(Timeline.time) != 0 && !before && !has_before) {
new_keyframe = this.createKeyframe({}, 0, channel, false, false);
}
- return {before, result, new_keyframe};
+ return { before, result, new_keyframe };
}
showContextMenu(event) {
Prop.active_panel = 'timeline'
@@ -140,13 +140,13 @@ class GeneralAnimator {
}
if (offset + el.clientHeight > scroll_top + height) {
$(timeline).animate({
- scrollTop: offset - (height-el.clientHeight-20)
+ scrollTop: offset - (height - el.clientHeight - 20)
}, 200);
}
}
}
}
-GeneralAnimator.addChannel = function(channel, options) {
+GeneralAnimator.addChannel = function (channel, options) {
this.prototype.channels[channel] = {
name: options.name || channel,
transform: options.transform || false,
@@ -155,16 +155,16 @@ GeneralAnimator.addChannel = function(channel, options) {
}
ModelProject.all.forEach(project => {
if (!project.animations)
- project.animations.forEach(animation => {
- animation.animators.forEach(animator => {
- if (animator instanceof this && !animator[channel]) {
- Vue.set(animator, channel, []);
- if (this.prototype.channels[channel].mutable) {
- Vue.set(animator.muted, channel, false);
+ project.animations.forEach(animation => {
+ animation.animators.forEach(animator => {
+ if (animator instanceof this && !animator[channel]) {
+ Vue.set(animator, channel, []);
+ if (this.prototype.channels[channel].mutable) {
+ Vue.set(animator.muted, channel, false);
+ }
}
- }
+ })
})
- })
})
Timeline.vue.$forceUpdate();
}
@@ -229,7 +229,7 @@ class BoneAnimator extends GeneralAnimator {
});
}
super.select();
-
+
if (this[Toolbox.selected.animation_channel] && (Timeline.selected.length == 0 || Timeline.selected[0].animator != this)) {
var nearest;
this[Toolbox.selected.animation_channel].forEach(kf => {
@@ -286,7 +286,7 @@ class BoneAnimator extends GeneralAnimator {
let e = keyframe.channel == 'scale' ? 1e4 : 1e2
ref.forEach((r, a) => {
if (!isNaN(r)) {
- ref[a] = Math.round(parseFloat(r)*e)/e
+ ref[a] = Math.round(parseFloat(r) * e) / e
}
})
}
@@ -347,7 +347,7 @@ class BoneAnimator extends GeneralAnimator {
let quat = bone.parent.getWorldQuaternion(Reusable.quat1);
quat.invert();
bone.quaternion.premultiply(quat);
-
+
}
return this;
}
@@ -373,7 +373,7 @@ class BoneAnimator extends GeneralAnimator {
var before = false
var after = false
var result = false
- let epsilon = 1/1200;
+ let epsilon = 1 / 1200;
function mapAxes(cb) {
if (!Animator._last_values[channel]) Animator._last_values[channel] = [0, 0, 0];
@@ -396,7 +396,7 @@ class BoneAnimator extends GeneralAnimator {
if (!before || keyframe.time > before.time) {
before = keyframe
}
- } else {
+ } else {
if (!after || keyframe.time < after.time) {
after = keyframe
}
@@ -418,7 +418,7 @@ class BoneAnimator extends GeneralAnimator {
} else {
let no_interpolations = Blockbench.hasFlag('no_interpolations')
let alpha = Math.getLerp(before.time, after.time, time)
- let {linear, step, catmullrom, bezier} = Keyframe.interpolation;
+ let { linear, step, catmullrom, bezier } = Keyframe.interpolation;
if (no_interpolations || (
before.interpolation === linear &&
@@ -433,8 +433,8 @@ class BoneAnimator extends GeneralAnimator {
let sorted = this[channel].slice().sort((kf1, kf2) => (kf1.time - kf2.time));
let before_index = sorted.indexOf(before);
- let before_plus = sorted[before_index-1];
- let after_plus = sorted[before_index+2];
+ let before_plus = sorted[before_index - 1];
+ let after_plus = sorted[before_index + 2];
if (this.animation.loop == 'loop' && sorted.length >= 3) {
if (!before_plus) before_plus = sorted.at(-2);
if (!after_plus) after_plus = sorted[1];
@@ -450,7 +450,7 @@ class BoneAnimator extends GeneralAnimator {
if (result && result instanceof Keyframe) {
let keyframe = result
let method = allow_expression ? 'get' : 'calc'
- let dp_index = (keyframe.time > time || Math.epsilon(keyframe.time, time, epsilon)) ? 0 : keyframe.data_points.length-1;
+ let dp_index = (keyframe.time > time || Math.epsilon(keyframe.time, time, epsilon)) ? 0 : keyframe.data_points.length - 1;
return mapAxes(axis => keyframe[method](axis, dp_index));
}
@@ -467,7 +467,7 @@ class BoneAnimator extends GeneralAnimator {
}
applyAnimationPreset(preset) {
let keyframes = [];
- Undo.initEdit({keyframes});
+ Undo.initEdit({ keyframes });
let current_time = Timeline.snapTime(Timeline.time);
for (let channel in this.channels) {
let timeline = preset[channel];
@@ -475,12 +475,14 @@ class BoneAnimator extends GeneralAnimator {
let data = {};
let value = timeline[timecode];
if (value instanceof Array) {
- data = {x: value[0], y: value[1], z: value[2]};
+ data = { x: value[0], y: value[1], z: value[2] };
} else if (value.pre) {
- data = {data_points: [
- {x: value.pre[0], y: value.pre[1], z: value.pre[2]},
- {x: value.post[0], y: value.post[1], z: value.post[2]},
- ]}
+ data = {
+ data_points: [
+ { x: value.pre[0], y: value.pre[1], z: value.pre[2] },
+ { x: value.post[0], y: value.post[1], z: value.post[2] },
+ ]
+ }
} else {
data = {
x: value.post[0], y: value.post[1], z: value.post[2],
@@ -500,30 +502,30 @@ class BoneAnimator extends GeneralAnimator {
return this;
}
}
- BoneAnimator.prototype.type = 'bone';
- BoneAnimator.prototype.channels = {
- rotation: {name: tl('timeline.rotation'), mutable: true, transform: true, max_data_points: 2},
- position: {name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2},
- scale: {name: tl('timeline.scale'), mutable: true, transform: true, max_data_points: 2},
- }
- Group.animator = BoneAnimator;
- BoneAnimator.prototype.menu = new Menu('bone_animator', [
- new MenuSeparator('settings'),
- {
- id: 'rotation_global',
- name: 'menu.animator.rotation_global',
- condition: animator => animator.type == 'bone',
- icon: (animator) => animator.rotation_global,
- click(animator) {
- Undo.initEdit({animations: [Animation.selected]});
- animator.rotation_global = !animator.rotation_global;
- Undo.finishEdit('Toggle rotation in global space');
- Animator.preview();
- }
- },
- new MenuSeparator('presets'),
- 'apply_animation_preset'
- ])
+BoneAnimator.prototype.type = 'bone';
+BoneAnimator.prototype.channels = {
+ rotation: { name: tl('timeline.rotation'), mutable: true, transform: true, max_data_points: 2 },
+ position: { name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2 },
+ scale: { name: tl('timeline.scale'), mutable: true, transform: true, max_data_points: 2 },
+}
+Group.animator = BoneAnimator;
+BoneAnimator.prototype.menu = new Menu('bone_animator', [
+ new MenuSeparator('settings'),
+ {
+ id: 'rotation_global',
+ name: 'menu.animator.rotation_global',
+ condition: animator => animator.type == 'bone',
+ icon: (animator) => animator.rotation_global,
+ click(animator) {
+ Undo.initEdit({ animations: [Animation.selected] });
+ animator.rotation_global = !animator.rotation_global;
+ Undo.finishEdit('Toggle rotation in global space');
+ Animator.preview();
+ }
+ },
+ new MenuSeparator('presets'),
+ 'apply_animation_preset'
+])
class NullObjectAnimator extends BoneAnimator {
constructor(uuid, animation, name) {
@@ -531,9 +533,6 @@ class NullObjectAnimator extends BoneAnimator {
this.uuid = uuid;
this._name = name;
- this.solver = new FIK.Structure3D(scene);
- this.chain = new FIK.Chain3D();
-
this.position = [];
}
get name() {
@@ -559,7 +558,7 @@ class NullObjectAnimator extends BoneAnimator {
this.element.select();
}
GeneralAnimator.prototype.select.call(this);
-
+
if (this[Toolbox.selected.animation_channel] && (Timeline.selected.length == 0 || Timeline.selected[0].animator != this)) {
var nearest;
this[Toolbox.selected.animation_channel].forEach(kf => {
@@ -593,12 +592,13 @@ class NullObjectAnimator extends BoneAnimator {
displayIK(get_samples) {
let null_object = this.getElement();
let target = [...Group.all, ...Locator.all].find(node => node.uuid == null_object.ik_target);
+ let pole = [...Group.all, ...Locator.all].find(node => node.uuid == null_object.ik_pole);
if (!null_object || !target) return;
let bones = [];
- let ik_target = new THREE.Vector3().copy(null_object.getWorldCenter(true));
+ let ik_target = null_object.getWorldCenter(true).clone();
let bone_references = [];
- let current = target.parent;
+ let current = target;
let source;
if (null_object.ik_source) {
@@ -621,52 +621,52 @@ class NullObjectAnimator extends BoneAnimator {
}
if (!bones.length) return;
bones.reverse();
-
+
bones.forEach(bone => {
if (bone.mesh.fix_rotation) bone.mesh.rotation.copy(bone.mesh.fix_rotation);
- })
+ });
+ let bone_pos = [];
bones.forEach((bone, i) => {
- let startPoint = new FIK.V3(0,0,0).copy(bone.mesh.getWorldPosition(new THREE.Vector3()));
- let endPoint = new FIK.V3(0,0,0).copy(bones[i+1] ? bones[i+1].mesh.getWorldPosition(new THREE.Vector3()) : null_object.getWorldCenter(false));
-
- let ik_bone = new FIK.Bone3D(startPoint, endPoint);
- this.chain.addBone(ik_bone);
-
- bone_references.push({
- bone,
- last_diff: new THREE.Vector3(
- (bones[i+1] ? bones[i+1] : target).origin[0] - bone.origin[0],
- (bones[i+1] ? bones[i+1] : target).origin[1] - bone.origin[1],
- (bones[i+1] ? bones[i+1] : target).origin[2] - bone.origin[2]
- ).normalize()
- })
- })
+ let pos = bone.mesh.getWorldPosition(new THREE.Vector3());
- this.solver.add(this.chain, ik_target , true);
- this.solver.meshChains[0].forEach(mesh => {
- mesh.visible = false;
- })
+ bone_pos.push(pos);
+
+ if (i != bones.length - 1) {
+ let last_diff = bones[i + 1].mesh.getWorldPosition(new THREE.Vector3());
+ bone.mesh.parent.worldToLocal(last_diff).sub(bone.mesh.position).normalize();
+
+ bone_references.push({
+ bone,
+ last_diff,
+ });
+ }
+ });
+
+ let polePos = pole.mesh.getWorldPosition(new THREE.Vector3());
+
+ fabrikIter(bone_pos, ik_target, polePos);
- this.solver.update();
-
let results = {};
- bone_references.forEach((bone_ref, i) => {
- let start = Reusable.vec1.copy(this.solver.chains[0].bones[i].start);
- let end = Reusable.vec2.copy(this.solver.chains[0].bones[i].end);
- bones[i].mesh.worldToLocal(start);
- bones[i].mesh.worldToLocal(end);
+ for (i = 0; i < bone_references.length; i++) {
+ let bone_ref = bone_references[i];
- Reusable.quat1.setFromUnitVectors(bone_ref.last_diff, end.sub(start).normalize());
- let rotation = get_samples ? new THREE.Euler() : Reusable.euler1;
- rotation.setFromQuaternion(Reusable.quat1, 'ZYX');
+ let end = bone_pos[i + 1];
+ bone_ref.bone.mesh.parent
+ .worldToLocal(end)
+ .sub(bone_ref.bone.mesh.position)
+ .normalize();
+
+ Reusable.quat1.setFromUnitVectors(
+ bone_ref.last_diff,
+ end,
+ );
- bone_ref.bone.mesh.rotation.x += rotation.x;
- bone_ref.bone.mesh.rotation.y += rotation.y;
- bone_ref.bone.mesh.rotation.z += rotation.z;
+ bone_ref.bone.mesh.applyQuaternion(Reusable.quat1);
bone_ref.bone.mesh.updateMatrixWorld();
if (get_samples) {
+ let rotation = new THREE.Euler().setFromQuaternion(Reusable.quat1, 'ZYX');
results[bone_ref.bone.uuid] = {
euler: rotation,
array: [
@@ -676,7 +676,7 @@ class NullObjectAnimator extends BoneAnimator {
]
}
}
- })
+ }
if (target_original_quaternion) {
let rotation = get_samples ? new THREE.Euler() : Reusable.euler1;
@@ -703,10 +703,6 @@ class NullObjectAnimator extends BoneAnimator {
}
}
- this.solver.clear();
- this.chain.clear();
- this.chain.lastTargetLocation.set(1e9, 0, 0);
-
if (get_samples) return results;
}
displayFrame(multiplier = 1) {
@@ -719,11 +715,11 @@ class NullObjectAnimator extends BoneAnimator {
}
}
}
- NullObjectAnimator.prototype.type = 'null_object';
- NullObjectAnimator.prototype.channels = {
- position: {name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2},
- }
- NullObject.animator = NullObjectAnimator;
+NullObjectAnimator.prototype.type = 'null_object';
+NullObjectAnimator.prototype.channels = {
+ position: { name: tl('timeline.position'), mutable: true, transform: true, max_data_points: 2 },
+}
+NullObject.animator = NullObjectAnimator;
class EffectAnimator extends GeneralAnimator {
constructor(animation) {
@@ -749,15 +745,15 @@ class EffectAnimator extends GeneralAnimator {
if (diff < 0) return;
let media = Timeline.playing_sounds.find(s => s.keyframe_id == kf.uuid);
- if (diff >= 0 && diff < (1/30) * (Timeline.playback_speed/100) && !media) {
+ if (diff >= 0 && diff < (1 / 30) * (Timeline.playback_speed / 100) && !media) {
if (kf.data_points[0].file && !kf.cooldown) {
media = new Audio(kf.data_points[0].file);
media.keyframe_id = kf.uuid;
- media.playbackRate = Math.clamp(Timeline.playback_speed/100, 0.1, 4.0);
- media.volume = Math.clamp(settings.volume.value/100, 0, 1);
- media.play().catch(() => {});
+ media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0);
+ media.volume = Math.clamp(settings.volume.value / 100, 0, 1);
+ media.play().catch(() => { });
Timeline.playing_sounds.push(media);
- media.onended = function() {
+ media.onended = function () {
Timeline.playing_sounds.remove(media);
}
@@ -765,13 +761,13 @@ class EffectAnimator extends GeneralAnimator {
setTimeout(() => {
delete kf.cooldown;
}, 400)
- }
+ }
} else if (diff > 0 && media) {
if (Math.abs(media.currentTime - diff) > 0.18 && diff < media.duration) {
console.log('Resyncing sound')
// Resync
media.currentTime = Math.clamp(diff + 0.08, 0, media.duration);
- media.playbackRate = Math.clamp(Timeline.playback_speed/100, 0.1, 4.0);
+ media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0);
}
}
})
@@ -790,14 +786,14 @@ class EffectAnimator extends GeneralAnimator {
let i_here = i;
let anim_uuid = this.animation.uuid;
emitter = particle_effect.emitters[kf.uuid + i] = new Wintersky.Emitter(WinterskyScene, particle_effect.config);
-
+
let old_variable_handler = emitter.Molang.variableHandler;
emitter.Molang.variableHandler = (key, params) => {
let curve_result = old_variable_handler.call(emitter, key, params);
if (curve_result !== undefined) return curve_result;
return Animator.MolangParser.variableHandler(key);
}
- emitter.on('start', ({params}) => {
+ emitter.on('start', ({ params }) => {
let animation = Animation.all.find(a => a.uuid === anim_uuid);
let kf_now = animation?.animators.effects?.particle.find(kf2 => kf2.uuid == kf.uuid);
let data_point_now = kf_now && kf_now.data_points[i_here];
@@ -820,12 +816,12 @@ class EffectAnimator extends GeneralAnimator {
} else if (emitter && emitter.enabled) {
emitter.stop(true);
}
- }
+ }
i++;
}
})
}
-
+
if (!this.muted.timeline) {
this.timeline.forEach(kf => {
if ((kf.time > this.last_displayed_time && kf.time <= this.animation.time) || Math.epsilon(kf.time, this.animation.time, 0.01)) {
@@ -844,13 +840,13 @@ class EffectAnimator extends GeneralAnimator {
var diff = kf.time - this.animation.time;
if (diff < 0 && Timeline.waveforms[kf.data_points[0].file] && Timeline.waveforms[kf.data_points[0].file].duration > -diff) {
var media = new Audio(kf.data_points[0].file);
- media.playbackRate = Math.clamp(Timeline.playback_speed/100, 0.1, 4.0);
- media.volume = Math.clamp(settings.volume.value/100, 0, 1);
+ media.playbackRate = Math.clamp(Timeline.playback_speed / 100, 0.1, 4.0);
+ media.volume = Math.clamp(settings.volume.value / 100, 0, 1);
media.currentTime = -diff;
media.keyframe_id = kf.uuid;
- media.play().catch(() => {});
+ media.play().catch(() => { });
Timeline.playing_sounds.push(media);
- media.onended = function() {
+ media.onended = function () {
Timeline.playing_sounds.remove(media);
}
@@ -858,18 +854,18 @@ class EffectAnimator extends GeneralAnimator {
setTimeout(() => {
delete kf.cooldown;
}, 400)
- }
+ }
}
})
}
}
}
- EffectAnimator.prototype.type = 'effect';
- EffectAnimator.prototype.channels = {
- particle: {name: tl('timeline.particle'), mutable: true, max_data_points: 1000},
- sound: {name: tl('timeline.sound'), mutable: true, max_data_points: 1000},
- timeline: {name: tl('timeline.timeline'), mutable: true, max_data_points: 1},
- }
+EffectAnimator.prototype.type = 'effect';
+EffectAnimator.prototype.channels = {
+ particle: { name: tl('timeline.particle'), mutable: true, max_data_points: 1000 },
+ sound: { name: tl('timeline.sound'), mutable: true, max_data_points: 1000 },
+ timeline: { name: tl('timeline.timeline'), mutable: true, max_data_points: 1 },
+}
StateMemory.init('animation_presets', 'array');
@@ -878,7 +874,7 @@ BARS.defineActions(() => {
condition: () => Modes.animate && Timeline.selected_animator && Timeline.selected_animator.applyAnimationPreset,
icon: 'library_books',
click: function (e) {
- new Menu('apply_animation_preset', this.children(), {searchable: true}).open(e.target);
+ new Menu('apply_animation_preset', this.children(), { searchable: true }).open(e.target);
},
children() {
let animator = Timeline.selected_animator;
@@ -903,17 +899,19 @@ BARS.defineActions(() => {
animator.applyAnimationPreset(preset);
},
children: [
- {icon: 'delete', name: 'generic.delete', click: () => {
- Blockbench.showMessageBox({
- title: 'generic.delete',
- message: 'generic.confirm_delete',
- buttons: ['dialog.confirm', 'dialog.cancel'],
- }, result => {
- if (result == 1) return;
- StateMemory.animation_presets.remove(preset);
- StateMemory.save('animation_presets');
- })
- }}
+ {
+ icon: 'delete', name: 'generic.delete', click: () => {
+ Blockbench.showMessageBox({
+ title: 'generic.delete',
+ message: 'generic.confirm_delete',
+ buttons: ['dialog.confirm', 'dialog.cancel'],
+ }, result => {
+ if (result == 1) return;
+ StateMemory.animation_presets.remove(preset);
+ StateMemory.save('animation_presets');
+ })
+ }
+ }
]
}
entries.push(entry);
@@ -924,17 +922,17 @@ BARS.defineActions(() => {
new Action('save_animation_preset', {
icon: 'playlist_add',
condition: () => Modes.animate && Keyframe.selected.length && Keyframe.selected.allAre(kf => kf.animator == Keyframe.selected[0].animator),
- click(event) {
+ click(event) {
let dialog = new Dialog({
id: 'save_animation_preset',
title: 'action.save_animation_preset',
width: 540,
form: {
- name: {label: 'generic.name'},
+ name: { label: 'generic.name' },
},
- onConfirm: function(formResult) {
+ onConfirm: function (formResult) {
if (!formResult.name) return;
-
+
let preset = {
uuid: guid(),
name: formResult.name,
diff --git a/js/outliner/null_object.js b/js/outliner/null_object.js
index 5ec33eff5..48694879f 100644
--- a/js/outliner/null_object.js
+++ b/js/outliner/null_object.js
@@ -111,6 +111,7 @@ class NullObject extends OutlinerElement {
new MenuSeparator('ik'),
'set_ik_target',
'set_ik_source',
+ 'set_ik_pole',
{
id: 'lock_ik_target_rotation',
name: 'menu.null_object.lock_ik_target_rotation',
@@ -136,6 +137,7 @@ class NullObject extends OutlinerElement {
new Property(NullObject, 'vector', 'position')
new Property(NullObject, 'string', 'ik_target', {condition: () => Format.animation_mode});
new Property(NullObject, 'string', 'ik_source', {condition: () => Format.animation_mode});
+ new Property(NullObject, 'string', 'ik_pole', { condition: () => Format.animation_mode });
new Property(NullObject, 'boolean', 'lock_ik_target_rotation')
new Property(NullObject, 'boolean', 'visibility', {default: true});
new Property(NullObject, 'boolean', 'locked');
@@ -296,4 +298,51 @@ BARS.defineActions(function() {
}
})
+
+ new Action('set_ik_pole', {
+ icon: 'fa-paperclip',
+ category: 'edit',
+ condition() {
+ let action = BarItems.set_ik_pole;
+ return NullObject.selected.length && action.children(action).length
+ },
+ searchable: true,
+ children() {
+ let nodes = [];
+ iterate(NullObject.selected[0].parent.children);
+
+ function iterate(arr) {
+ arr.forEach(node => {
+ if (node instanceof Group) {
+ nodes.push(node);
+ iterate(node.children);
+ }
+ if (node instanceof Locator) {
+ nodes.push(node);
+ }
+ })
+ }
+ return nodes.map(node => {
+ return {
+ name: node.name + (node.uuid == NullObject.selected[0].ik_pole ? ' (✔)' : ''),
+ icon: node instanceof Locator ? 'fa-anchor' : 'folder',
+ color: markerColors[node.color % markerColors.length] && markerColors[node.color % markerColors.length].standard,
+ click() {
+ Undo.initEdit({ elements: NullObject.selected });
+ NullObject.selected.forEach(null_object => {
+ if (null_object.ik_pole == node.uuid) {
+ null_object.ik_pole = undefined;
+ } else {
+ null_object.ik_pole = node.uuid;
+ }
+ })
+ Undo.finishEdit('Set IK pole');
+ }
+ }
+ })
+ },
+ click(event) {
+ new Menu('set_ik_pole', this.children(this), { searchable: true }).show(event.target, this);
+ }
+ })
})
diff --git a/lang/en.json b/lang/en.json
index 1100de5f5..af7b23cd2 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1849,6 +1849,8 @@
"action.set_ik_target.desc": "Select the target bone that should be moved by this null object via Inverse Kinematics",
"action.set_ik_source": "Set IK Root",
"action.set_ik_source.desc": "Select the root bone of the chain that should be moved by this null object via Inverse Kinematics",
+ "action.set_ik_pole": "Set IK Pole",
+ "action.set_ik_pole.desc": "Select the pole that the chain will attempt to face towards while moving",
"action.lock_motion_trail": "Lock Motion Trail",
"action.lock_motion_trail.desc": "Lock the motion trail to the currently selected group",
"action.animation_onion_skin": "Animation Onion Skin",
diff --git a/lib/fik.min.js b/lib/fik.min.js
deleted file mode 100644
index 3b20cbde9..000000000
--- a/lib/fik.min.js
+++ /dev/null
@@ -1,82 +0,0 @@
-(function(h,l){"object"===typeof exports&&"undefined"!==typeof module?l(exports):"function"===typeof define&&define.amd?define(["exports"],l):l(h.FIK={})})(this,function(h){function l(a,b){this.x=a||0;this.y=b||0}function f(a,b,c){this.x=a||0;this.y=b||0;this.z=c||0}function y(){this.elements=[1,0,0,0,1,0,0,0,1];0a?-1:0=c?Math.PI:1<=c?0:Math.acos(c);return 0<=a.end.x*b.end.y-a.end.y*b.end.x?c:-c},clamp:function(a,b,c){a=ac?c:a},lerp:function(a,b,c){return(1-c)*a+c*b},rand:function(a,b){return a+Math.random()*(b-a)},randInt:function(a,b){return a+Math.floor(Math.random()*(b-a+1))},nearEquals:function(a,b,c){return Math.abs(a-b)<=c?!0:!1},perpendicular:function(a,b){return m.nearEquals(a.dot(b),0,.01)?!0:!1},genPerpendicularVectorQuick:function(a){var b=
-a.clone();return.99>Math.abs(a.y)?b.set(-a.z,0,a.x).normalize():b.set(0,a.z,-a.y).normalize()},genPerpendicularVectorFrisvad:function(a){var b=a.clone();if(-.9999999>a.z)return b.set(0,-1,0);var c=1/(1+a.z);return b.set(1-a.x*a.x*c,-a.x*a.y*c,-a.x).normalize()},rotateXDegs:function(a,b){return a.clone().rotate(b*m.toRad,"X")},rotateYDegs:function(a,b){return a.clone().rotate(b*m.toRad,"Y")},rotateZDegs:function(a,b){return a.clone().rotate(b*m.toRad,"Z")},withinManhattanDistance:function(a,b,c){return Math.abs(b.x-
-a.x)>c||Math.abs(b.y-a.y)>c||Math.abs(b.z-a.z)>c?!1:!0},manhattanDistanceBetween:function(a,b){return Math.abs(b.x-a.x)+Math.abs(b.x-a.x)+Math.abs(b.x-a.x)},distanceBetween:function(a,b){var c=b.x-a.x,d=b.y-a.y;a=void 0!==a.z?b.z-a.z:0;return Math.sqrt(c*c+d*d+a*a)},rotateDegs:function(a,b){return a.clone().rotate(b*m.toRad)},validateDirectionUV:function(a){0>a.length()&&p.error("vector direction unit vector cannot be zero.")},validateLength:function(a){0>a&&p.error("Length must be a greater than or equal to zero.")}};
-Object.assign(l.prototype,{isVector2:!0,set:function(a,b){this.x=a||0;this.y=b||0;return this},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x;a=this.y-a.y;return b*b+a*a},multiplyScalar:function(a){this.x*=a;this.y*=a;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},normalize:function(){return this.divideScalar(this.length()||1)},normalised:function(){return(new l(this.x,
-this.y)).normalize()},lengthSq:function(){return this.x*this.x+this.y*this.y},add:function(a){this.x+=a.x;this.y+=a.y;return this},plus:function(a){return new l(this.x+a.x,this.y+a.y)},min:function(a){this.x-=a.x;this.y-=a.y;return this},minus:function(a){return new l(this.x-a.x,this.y-a.y)},divideBy:function(a){return(new l(this.x,this.y)).divideScalar(a)},times:function(a){return a.isVector2?new l(this.x*a.x,this.y*a.y):new l(this.x*a,this.y*a,this.z*a)},dot:function(a,b){return this.x*a.x+this.y*
-a.y},negate:function(){this.x=-this.x;this.y=-this.y;return this},negated:function(){return new l(-this.x,-this.y)},clone:function(){return new l(this.x,this.y)},copy:function(a){this.x=a.x;this.y=a.y;return this},cross:function(a){return this.x*a.y-this.y*a.x},sign:function(a){return 0<=this.cross(a)?1:-1},approximatelyEquals:function(a,b){if(0>b)return!1;var c=Math.abs(this.y-a.y);return Math.abs(this.x-a.x)=a?Math.PI:1<=a?0:Math.acos(a)},getSignedAngle:function(a){var b=this.angleTo(a);return 1===this.sign(a)?b:-b},constrainedUV:function(a,b,c){var d=a.getSignedAngle(this);d>c&&this.copy(a).rotate(c);dthis.x?-this.x:this.x,0>this.y?-this.y:this.y,0>this.z?-this.z:this.z)},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},normalize:function(){return this.divideScalar(this.length()||1)},normalised:function(){return(new f(this.x,
-this.y,this.z)).normalize()},add:function(a){this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},min:function(a){this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},plus:function(a){return new f(this.x+a.x,this.y+a.y,this.z+a.z)},minus:function(a){return new f(this.x-a.x,this.y-a.y,this.z-a.z)},divideBy:function(a){return new f(this.x/a,this.y/a,this.z/a)},multiply:function(a){return new f(this.x*a,this.y*a,this.z*a)},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;return this},divideScalar:function(a){return this.multiplyScalar(1/
-a)},cross:function(a){return new f(this.y*a.z-this.z*a.y,this.z*a.x-this.x*a.z,this.x*a.y-this.y*a.x)},crossVectors:function(a,b){var c=a.x,d=a.y;a=a.z;var e=b.x,g=b.y;b=b.z;this.x=d*b-a*g;this.y=a*e-c*b;this.z=c*g-d*e;return this},negate:function(){this.x=-this.x;this.y=-this.y;this.z=-this.z;return this},negated:function(){return new f(-this.x,-this.y,-this.z)},clone:function(){return new f(this.x,this.y,this.z)},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this},approximatelyEquals:function(a,
-b){if(0>b)return!1;var c=Math.abs(this.y-a.y),d=Math.abs(this.z-a.z);return Math.abs(this.x-a.x)=a?Math.PI:1<=a?0:Math.acos(a)},getSignedAngle:function(a,b){var c=this.angleTo(a);return 1===this.sign(a,
-b)?c:-c},constrainedUV:function(a,b,c,d,e){var g=a.getSignedAngle(this,b);g>e&&this.copy(c.rotateAboutAxis(a,e,b));gc){var d=a.normalised().cross(this).normalize();this.copy(b.rotateAboutAxis(a,c,d))}return this}});Object.assign(y.prototype,{isMatrix3:!0,set:function(a,b,c,d,e,g,k,h,t){var f=this.elements;f[0]=a;f[1]=d;f[2]=k;f[3]=b;f[4]=e;f[5]=h;f[6]=c;f[7]=g;f[8]=t;return this},identity:function(){this.set(1,
-0,0,0,1,0,0,0,1);return this},setV3:function(a,b,c){var d=this.elements;d[0]=a.x;d[3]=a.y;d[6]=a.z;d[1]=b.x;d[4]=b.y;d[7]=b.z;d[2]=c.x;d[5]=c.y;d[8]=c.z;return this},transpose:function(){var a=this.elements;var b=a[1];a[1]=a[3];a[3]=b;b=a[2];a[2]=a[6];a[6]=b;b=a[5];a[5]=a[7];a[7]=b;return this},createRotationMatrix:function(a){var b=new f(1,0,0),c=new f(0,1,0);if(-.9999999>a.z)b.set(1,0,0),c.set(0,1,0);else{var d=1/(1+a.z),e=-a.x*a.y*d;b.set(1-a.x*a.x*d,e,-a.x).normalize();c.set(e,1-a.y*a.y*d,-a.y).normalize()}return this.setV3(b,
-c,a)},rotateAboutAxis:function(a,b,c){var d=Math.sin(b);b=Math.cos(b);var e=1-b,g=c.x*c.y*e,k=c.x*c.z*e,h=c.y*c.z*e,f=this.elements;f[0]=c.x*c.x*e+b;f[3]=g+c.z*d;f[6]=k-c.y*d;f[1]=g-c.z*d;f[4]=c.y*c.y*e+b;f[7]=h+c.x*d;f[2]=k+c.y*d;f[5]=h-c.x*d;f[8]=c.z*c.z*e+b;return a.clone().applyM3(this)}});Object.assign(u,{slerp:function(a,b,c,d){return c.copy(a).slerp(b,d)}});Object.defineProperties(u.prototype,{x:{get:function(){return this._x},set:function(a){this._x=a;this.onChangeCallback()}},y:{get:function(){return this._y},
-set:function(a){this._y=a;this.onChangeCallback()}},z:{get:function(){return this._z},set:function(a){this._z=a;this.onChangeCallback()}},w:{get:function(){return this._w},set:function(a){this._w=a;this.onChangeCallback()}}});Object.assign(u.prototype,{set:function(a,b,c,d){this._x=a;this._y=b;this._z=c;this._w=d;this.onChangeCallback();return this},clone:function(){return new this.constructor(this._x,this._y,this._z,this._w)},setFromAxisAngle:function(a,b){b/=2;var c=Math.sin(b);this._x=a.x*c;this._y=
-a.y*c;this._z=a.z*c;this._w=Math.cos(b);this.onChangeCallback();return this},copy:function(a){this._x=a.x;this._y=a.y;this._z=a.z;this._w=a.w;this.onChangeCallback();return this},setFromRotationMatrix:function(a){var b=a.elements,c=b[0];a=b[4];var d=b[8],e=b[1],g=b[5],k=b[9],f=b[2],h=b[6];b=b[10];var l=c+g+b;0g&&c>b?(c=2*Math.sqrt(1+c-g-b),this._w=(h-k)/c,this._x=.25*c,this._y=(a+e)/c,this._z=(d+f)/c):g>b?(c=
-2*Math.sqrt(1+g-c-b),this._w=(d-f)/c,this._x=(a+e)/c,this._y=.25*c,this._z=(k+h)/c):(c=2*Math.sqrt(1+b-c-g),this._w=(e-a)/c,this._x=(d+f)/c,this._y=(k+h)/c,this._z=.25*c);this.onChangeCallback();return this},setFromUnitVectors:function(){var a=new f,b;return function(c,d){void 0===a&&(a=new f);b=c.dot(d)+1;1E-6>b?(b=0,Math.abs(c.x)>Math.abs(c.z)?a.set(-c.y,c.x,0):a.set(0,-c.z,c.y)):a.crossVectors(c,d);this._x=a.x;this._y=a.y;this._z=a.z;this._w=b;return this.normalize()}}(),inverse:function(){return this.conjugate()},
-conjugate:function(){this._x*=-1;this._y*=-1;this._z*=-1;this.onChangeCallback();return this},dot:function(a){return this._x*a._x+this._y*a._y+this._z*a._z+this._w*a._w},lengthSq:function(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w},length:function(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)},normalize:function(){var a=this.length();0===a?(this._z=this._y=this._x=0,this._w=1):(a=1/a,this._x*=a,this._y*=a,this._z*=a,this._w*=a);this.onChangeCallback();
-return this},multiply:function(a,b){return void 0!==b?(console.warn("THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(a,b)):this.multiplyQuaternions(this,a)},premultiply:function(a){return this.multiplyQuaternions(a,this)},multiplyQuaternions:function(a,b){var c=a._x,d=a._y,e=a._z;a=a._w;var g=b._x,k=b._y,f=b._z;b=b._w;this._x=c*b+a*g+d*f-e*k;this._y=d*b+a*k+e*g-c*f;this._z=e*b+a*f+c*k-d*g;this._w=a*b-c*g-d*k-e*f;this.onChangeCallback();
-return this},onChangeCallback:function(){}});var q=Math.PI,n=Math.PI/180,G=180/Math.PI,H=new f(1,0,0),I=new f(0,1,0),J=new f(0,0,1),K=new f(-1,0,0),L=new f(0,-1,0),M=new f(0,0,-1),F=new l(0,1),N=new l(0,-1),O=new l(-1,0),P=new l(1,0);Object.assign(v.prototype,{isJoint3D:!0,clone:function(){var a=new v;a.type=this.type;a.rotor=this.rotor;a.max=this.max;a.min=this.min;a.freeHinge=this.freeHinge;a.rotationAxisUV.copy(this.rotationAxisUV);a.referenceAxisUV.copy(this.referenceAxisUV);return a},testAngle:function(){this.freeHinge=
-this.max===q&&this.min===-q?!0:!1},validateAngle:function(a){a=0>a?0:a;return 180c&&(c*=-1);this.min=-this.validateAngle(c)*n;this.max=this.validateAngle(d)*n;this.testAngle();this.rotationAxisUV.copy(b).normalize();this.referenceAxisUV.copy(e).normalize()},getHingeReferenceAxis:function(){return this.referenceAxisUV},getHingeRotationAxis:function(){return this.rotationAxisUV},
-setBallJointConstraintDegs:function(a){this.rotor=this.validateAngle(a)*n},setHingeClockwise:function(a){0>a&&(a*=-1);this.min=-this.validateAngle(a)*n;this.testAngle()},setHingeAnticlockwise:function(a){this.max=this.validateAngle(a)*n;this.testAngle()}});Object.assign(r.prototype,{isBone3D:!0,init:function(a,b,c,d){this.setStartLocation(a);b?(this.setEndLocation(b),this.length=this.getLength()):(this.setLength(d),this.setEndLocation(this.start.plus(c.normalised().multiplyScalar(d))))},clone:function(){var a=
-new r(this.start,this.end);a.joint=this.joint.clone();return a},setColor:function(a){this.color=a},setBoneConnectionPoint:function(a){this.boneConnectionPoint=a},setHingeClockwise:function(a){this.joint.setHingeClockwise(a)},setHingeAnticlockwise:function(a){this.joint.setHingeAnticlockwise(a)},setBallJointConstraintDegs:function(a){this.joint.setBallJointConstraintDegs(a)},setStartLocation:function(a){this.start.copy(a)},setEndLocation:function(a){this.end.copy(a)},setLength:function(a){0=b.length()?p.error("Hinge rotation axis cannot be zero."):0>=e.length()?p.error("Hinge reference axis cannot be zero."):m.perpendicular(b,e)?(a=a||"global",this.baseboneConstraintType="global"===a?3:5,this.baseboneConstraintUV=b.normalised(),this.bones[0].joint.setHinge("global"===a?12:11,b,c,d,e)):p.error("The hinge reference axis must be in the plane of the hinge rotation axis, that is, they must be perpendicular.")},setFreelyRotatingGlobalHingedBasebone:function(a){this.setHingeBaseboneConstraint("global",
-a,180,180,m.genPerpendicularVectorQuick(a))},setGlobalHingedBasebone:function(a,b,c,d){this.setHingeBaseboneConstraint("global",a,b,c,d)},setFreelyRotatingLocalHingedBasebone:function(a){this.setHingeBaseboneConstraint("local",a,180,180,m.genPerpendicularVectorQuick(a))},setLocalHingedBasebone:function(a,b,c,d){this.setHingeBaseboneConstraint("local",a,b,c,d)},setBaseLocation:function(a){this.baseLocation.copy(a)},setFixedBaseMode:function(a){if(a||-1===this.connectedChainNumber)if(2!==this.baseboneConstraintType||
-a)this.fixedBaseMode=a},setMaxIterationAttempts:function(a){1>a||(this.maxIteration=a)},setMinIterationChange:function(a){0>a||(this.minIterationChange=a)},setSolveDistanceThreshold:function(a){0>a||(this.solveDistanceThreshold=a)},solveForEmbeddedTarget:function(){if(this.useEmbeddedTarget)return this.solveForTarget(this.embeddedTarget)},resetTarget:function(){this.lastBaseLocation=new f(Infinity,Infinity,Infinity);this.currentSolveDistance=Infinity},solveForTarget:function(a){this.tmpTarget.set(a.x,
-a.y,a.z);a=this.precision;var b=this.lastBaseLocation.approximatelyEquals(this.baseLocation,a);if(this.lastTargetLocation.approximatelyEquals(this.tmpTarget,a)&&b)return this.currentSolveDistance;a=null;b?(b=this.bones[this.numBones-1].end.distanceTo(this.tmpTarget),a=this.cloneBones()):b=Infinity;for(var c=[],d=Infinity,e=Infinity,g,k=this.maxIteration;k--;){g=this.solveIK(this.tmpTarget);if(gthis.numChains||c>this.chains[b].numBones)){a=a.clone();void 0!==f&&a.setColor(f);a.setBoneConnectionPoint("end"===d?21:20);a.setConnectedChainNumber(b);a.setConnectedBoneNumber(c);b="end"===d?this.chains[b].bones[c].end:this.chains[b].bones[c].start;a.setBaseLocation(b);a.setFixedBaseMode(!0);for(c=0;c<
-a.numBones;c++)a.bones[c].start.add(b),a.bones[c].end.add(b);this.add(a,e,g)}},addChainMeshs:function(a,b){this.isWithMesh=!0;b=[];for(var c=a.bones.length,d=0;da?0:a;return 180a||(this.maxIteration=a)},setMinIterationChange:function(a){0>a||(this.minIterationChange=a)},setSolveDistanceThreshold:function(a){0>a||(this.solveDistanceThreshold=a)},solveForEmbeddedTarget:function(){if(this.useEmbeddedTarget)return this.solveForTarget(this.embeddedTarget)},
-resetTarget:function(){this.lastBaseLocation=new l(Infinity,Infinity);this.currentSolveDistance=Infinity},solveForTarget:function(a){this.tmpTarget.set(a.x,a.y);a=this.precision;var b=this.lastBaseLocation.approximatelyEquals(this.baseLocation,a);if(this.lastTargetLocation.approximatelyEquals(this.tmpTarget,a)&&b)return this.currentSolveDistance;a=null;b?(b=this.bones[this.numBones-1].end.distanceTo(this.tmpTarget),a=this.cloneBones()):b=Infinity;for(var c=[],d=Infinity,e=Infinity,g,f=this.maxIteration;f--;){g=
-this.solveIK(this.tmpTarget);if(gthis.numChains)p.error("Chain not existe !");else if(c>this.chains[b].numBones)p.error("Bone not existe !");else{a=a.clone();a.setBoneConnectionPoint("end"===d?21:20);a.setConnectedChainNumber(b);a.setConnectedBoneNumber(c);b="end"===d?this.chains[b].bones[c].end:this.chains[b].bones[c].start;a.setBaseLocation(b);a.setFixedBaseMode(!0);for(c=0;c