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