Merge pull request #2305 from niacdoial/newrotation

Added a rotation node-socket
This commit is contained in:
Lubos Lenco 2021-08-26 09:53:25 +02:00 committed by GitHub
commit 85edde6d24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1637 additions and 448 deletions

View file

@ -1,10 +1,13 @@
package armory.logicnode;
import iron.object.Object;
import iron.math.Vec3;
import iron.math.Quat;
import iron.math.Vec4;
class GetRotationNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
@ -16,34 +19,16 @@ class GetRotationNode extends LogicNode {
return null;
}
var rot = object.transform.rot;
switch (from) {
case 0:
// euler angles
return object.transform.rot.getEuler();
case 1:
// vector
var sqrtW = Math.sqrt(1 - (rot.w * rot.w));
if (sqrtW == 0) {
return new Vec3(0, 0, 1);
}
return new Vec3(rot.x / sqrtW, rot.y / sqrtW, rot.z / sqrtW);
case 2:
// angle radians
var angle = 2 * Math.acos(rot.w);
return angle;
case 3:
// angle degrees
var angle = 2 * Math.acos(rot.w);
return angle * (180 / Math.PI);
case 4:
//quaternion xyz
return new Vec3(rot.x, rot.y, rot.z);
case 5:
//quaternion w
return rot.w;
}
switch(property0){
case "Local":
return object.transform.rot;
case "Global":{
var useless1 = new Vec4();
var ret = new Quat();
object.transform.world.decompose(useless1, ret, useless1);
return ret;
}}
return null;
}
}

View file

@ -37,6 +37,6 @@ class LookAtNode extends LogicNode {
v2.setFrom(vto).sub(vfrom).normalize();
q.fromTo(v1, v2);
return q.getEuler();
return q;
}
}

View file

@ -19,7 +19,7 @@ class QuaternionMathNode extends LogicNode {
override function get(from: Int): Dynamic {
switch (property0) {
// 1 argument: Module, Normalize, GetEuler
// 1 argument: Module, Normalize
case "Module": {
var q: Quat = inputs[0].get();
if (q == null) return null;
@ -32,159 +32,106 @@ class QuaternionMathNode extends LogicNode {
res_q.setFrom(q);
res_q = res_q.normalize();
}
case "GetEuler": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
res_v = res_q.getEuler();
}
// 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle
case "FromTo": {
var v1: Vec4 = inputs[0].get();
var v2: Vec4 = inputs[1].get();
if ((v1 == null) || (v2 == null)) return null;
res_q.fromTo(v1, v2);
}
case "FromMat": {
var q: Quat = inputs[0].get();
var m: Mat4 = inputs[1].get();
if ((q == null) || (m == null)) return null;
res_q.setFrom(q);
res_q = res_q.fromMat(m);
}
case "FromRotationMat": {
var q: Quat = inputs[0].get();
var m: Mat4 = inputs[1].get();
if ((q == null) || (m == null)) return null;
res_q.setFrom(q);
res_q = res_q.fromRotationMat(m);
}
case "ToAxisAngle": {
var q: Quat = inputs[0].get();
var v: Vec4 = inputs[1].get();
if ((q == null) || (v == null)) return null;
res_q.setFrom(q);
res_f = res_q.toAxisAngle(v);
}
// # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler
case "Lerp": {
var from: Quat = inputs[0].get();
var to: Quat = inputs[1].get();
var f: Float = inputs[2].get();
if ((from == null) || (to == null)) return null;
res_q = res_q.lerp(from, to, f);
}
case "Slerp": {
var from: Quat = inputs[0].get();
var to: Quat = inputs[1].get();
var f: Float = inputs[2].get();
if ((from == null) || (to == null)) return null;
res_q = res_q.slerp(from, to, f);
}
case "FromAxisAngle": {
var q: Quat = inputs[0].get();
var axis: Vec4 = inputs[1].get();
var angle: Float = inputs[2].get();
if ((q == null) || (axis == null)) return null;
res_q.setFrom(q);
res_q = res_q.fromAxisAngle(axis, angle);
}
case "FromEuler": {
var x: Float = inputs[0].get();
var y: Float = inputs[1].get();
var z: Float = inputs[2].get();
res_q = res_q.fromEuler(x, y, z);
}
// Many arguments: Add, Subtract, DotProduct, Multiply
case "Add": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
var q2 = new Quat();
res_v = inputs[0].get();
res_f = inputs[1].get();
if (res_v == null || res_f == null) return null;
res_q.set(res_v.x, res_v.y, res_v.z, res_f);
var i = 1;
while (i < inputs.length) {
q2 = inputs[i].get();
if (q2 == null) return null;
res_q.add(q2);
while (2*i+1 < inputs.length) {
res_v = inputs[2*i].get();
res_f = inputs[2*i+1].get();
if (res_v == null || res_f == null) return null;
res_q.x += res_v.x;
res_q.y += res_v.y;
res_q.z += res_v.z;
res_q.w += res_f;
i++;
}
}
case "Subtract": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
var q2 = new Quat();
res_v = inputs[0].get();
res_f = inputs[1].get();
if (res_v == null || res_f == null) return null;
res_q.set(res_v.x, res_v.y, res_v.z, res_f);
var i = 1;
while (i < inputs.length) {
q2 = inputs[i].get();
if (q2 == null) return null;
res_q.sub(q2);
while (2*i+1 < inputs.length) {
res_v = inputs[2*i].get();
res_f = inputs[2*i+1].get();
if (res_v == null || res_f == null) return null;
res_q.x -= res_v.x;
res_q.y -= res_v.y;
res_q.z -= res_v.z;
res_q.w -= res_f;
i++;
}
}
case "Multiply": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
var q2 = new Quat();
res_v = inputs[0].get();
res_f = inputs[1].get();
if (res_v == null || res_f == null) return null;
res_q.set(res_v.x, res_v.y, res_v.z, res_f);
var i = 1;
while (i < inputs.length) {
q2 = inputs[i].get();
if (q2 == null) return null;
res_q.mult(q2);
while (2*i+1 < inputs.length) {
res_v = inputs[2*i].get();
res_f = inputs[2*i+1].get();
if (res_v == null || res_f == null) return null;
var temp_q = new Quat(res_v.x, res_v.y, res_v.z, res_f);
res_q.mult(temp_q);
i++;
}
}
case "MultiplyFloats": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
res_v = inputs[0].get();
res_f = inputs[1].get();
if (res_v == null || res_f == null) return null;
res_q.set(res_v.x, res_v.y, res_v.z, res_f);
var f: Float = 1.0;
var i = 1;
var i = 2;
while (i < inputs.length) {
f = inputs[i].get();
res_q.scale(f);
f *= inputs[i].get();
if (f == null) return null;
i++;
}
res_q.scale(f);
}
case "DotProduct": {
var q: Quat = inputs[0].get();
if (q == null) return null;
res_q.setFrom(q);
var q2 = new Quat();
case "DotProduct": { // what this does with more than 2 terms is not *remotely* intuitive. Heck, you could consider it a footgun!
res_v = inputs[0].get();
var temp_f = inputs[1].get();
if (res_v == null || temp_f == null) return null;
res_q.set(res_v.x, res_v.y, res_v.z, temp_f);
var i = 1;
while (i < inputs.length) {
q2 = inputs[i].get();
if (q2 == null) return null;
res_f = res_q.dot(q2);
while (2*i+1 < inputs.length) {
res_v = inputs[2*i].get();
temp_f = inputs[2*i+1].get();
if (res_v == null || temp_f == null) return null;
var temp_q = new Quat(res_v.x, res_v.y, res_v.z, temp_f);
res_f = res_q.dot(temp_q);
res_q.set(res_f, res_f, res_f, res_f);
i++;
}
}
}
// Return and check separator
switch (from) {
case 0: {
if (property0 == 'GetEuler')
return res_v;
else
return res_q;
return res_q;
}
case 1:
if (property1) {
return res_q.x;
} else {
if (property0 == "DotProduct" || property0 == "Module") {
return res_f;
} else {
return null;
}
case 2:
if (property1) return res_q.y;
case 3:
if (property1) return res_q.z;
case 4:
if (property1) return res_q.w;
case 5:
if (property1) return res_f;
default: {
return null;
}
}
return null;
}
}

View file

@ -2,13 +2,11 @@ package armory.logicnode;
import iron.object.Object;
import iron.math.Quat;
import iron.math.Vec4;
import armory.trait.physics.RigidBody;
class RotateObjectNode extends LogicNode {
public var property0 = "Euler Angles";
var q = new Quat();
public var property0 = "Local";
public function new(tree: LogicTree) {
super(tree);
@ -16,32 +14,19 @@ class RotateObjectNode extends LogicNode {
override function run(from: Int) {
var object: Object = inputs[1].get();
var vec: Vec4 = inputs[2].get();
var q: Quat = inputs[2].get();
// note: here, the next line is disabled because old versions of the node don't have a third input.
// when those old versions will be considered remove, feel free to uncomment that, and replace the other `inputs[3].get()` by `w` in this file.
//var w: Float = inputs[3].get();
if (object == null || q == null) return;
if (object == null || vec == null) return;
switch (property0) {
case "Euler Angles":
q.fromEuler(vec.x, vec.y, vec.z);
case "Angle Axies (Degrees)" | "Angle Axies (Radians)":
var angle: Float = inputs[3].get();
if (property0 == "Angle Axies (Degrees)") {
angle = angle * (Math.PI / 180);
}
var angleSin = Math.sin(angle / 2);
vec = vec.normalize();
var angleCos = Math.cos(angle / 2);
q = new Quat(vec.x * angleSin, vec.y * angleSin, vec.z * angleSin, angleCos);
case "Quaternion":
q = new Quat(vec.x, vec.y, vec.z, inputs[3].get());
q.normalize();
q.normalize();
switch (property0){
case "Local":
object.transform.rot.mult(q);
case "Global":
object.transform.rot.multquats(q, object.transform.rot);
// that function call (Quat.multquats) is weird: it both modifies the object, and returns `this`
}
object.transform.rot.mult(q);
object.transform.buildMatrix();
#if arm_physics

View file

@ -0,0 +1,96 @@
package armory.logicnode;
import iron.math.Quat;
import iron.math.Vec4;
import iron.math.Mat4;
import kha.FastFloat;
class RotationMathNode extends LogicNode {
public var property0: String; // Operation
var res_q = new Quat();
var res_v = new Vec4();
var res_f: FastFloat = 0.0;
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
//var q: Quat = inputs[0].get();
//if (q==null) return null;
//var res_q: Quat = new Quat();
switch (property0) {
// 1 argument: Normalize, Inverse
case "Normalize": {
var q: Quat = inputs[0].get();
if (q==null) return null;
res_q.setFrom(q);
res_q = res_q.normalize();
}
case "Inverse": {
var q: Quat = inputs[0].get();
if (q==null) return null;
var modl = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;
modl = -1/modl;
res_q.w = -q.w*modl;
res_q.x = q.x*modl;
res_q.y = q.y*modl;
res_q.z = q.z*modl;
}
// 2 arguments: Compose, Amplify, FromTo, FromRotationMat,
case "FromTo": {
var v1: Vec4 = inputs[0].get();
var v2: Vec4 = inputs[1].get();
if ((v1 == null) || (v2 == null)) return null;
res_q.fromTo(v1, v2);
}
case "Compose": {
var v1: Quat = inputs[0].get();
var v2: Quat = inputs[1].get();
if ((v1 == null) || (v2 == null)) return null;
res_q.multquats(v1,v2);
}
case "Amplify": {
var v1: Quat = inputs[0].get();
var v2: Float = inputs[1].get();
if ((v1 == null) || (v2 == null)) return null;
res_q.setFrom(v1);
var fac2 = Math.sqrt(1- res_q.w*res_q.w);
if (fac2 > 0.001) {
var fac1 = v2*Math.acos(res_q.w);
res_q.w = Math.cos(fac1);
fac1 = Math.sin(fac1)/fac2;
res_q.x *= fac1;
res_q.y *= fac1;
res_q.z *= fac1;
}
}
//case "FromRotationMat": {
// var m: Mat4 = inputs[1].get();
// if (m == null) return null;
// res_q = res_q.fromMat(m);
//}
// # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler
case "Lerp": {
//var from = q;
var from: Quat = inputs[0].get();
var to: Quat = inputs[1].get();
var f: Float = inputs[2].get();
if ((from == null) || (f == null) || (to == null)) return null;
res_q = res_q.lerp(from, to, f);
}
case "Slerp": {
//var from = q;
var from:Quat = inputs[0].get();
var to: Quat = inputs[1].get();
var f: Float = inputs[2].get();
if ((from == null) || (f == null) || (to == null)) return null;
res_q = res_q.slerp(from, to, f);
}
}
return res_q;
}
}

View file

@ -0,0 +1,120 @@
package armory.logicnode;
import iron.math.Vec4;
import iron.math.Quat;
import kha.FastFloat;
class RotationNode extends LogicNode {
static inline var toRAD: FastFloat = 0.017453292519943295; // 180/pi
public var property0: String; // type of input (EulerAngles, AxisAngle, Quaternion)
public var property1: String; // angle unit (Deg, Rad)
public var property2: String; // euler order (XYZ, XZY, etc…)
public var value: Quat;
//var input0_cache: Vec4 = new Vec4();
//var input1_cache: Float = 0;
var input_length: Int = 0;
public function new(tree: LogicTree, x: Null<Float> = null,
y: Null<Float> = null,
z: Null<Float> = null,
w: Null<Float> = null
) {
super(tree);
this.value = new Quat();
if (x!=null) this.value.set(x,y,z,w);
for (input in inputs) {
if (input !=null)
this.input_length +=1;
else
break;
}
}
override function get(from: Int): Dynamic {
//var inp0 = inputs[0].get();
//var inp
//if (inputs[0].get())
if (inputs.length == 0){
return this.value;
}
switch (property0){
case "Quaternion": {
if (inputs[0]!=null && inputs[1]!=null) {
var vect: Vec4 = inputs[0].get();
value.x = vect.x;
value.y = vect.y;
value.z = vect.z;
value.w = inputs[1].get();
}
}
case "AxisAngle": {
if (inputs[0]!=null && inputs[1]!=null){
var vec: Vec4 = inputs[0].get();
var angle: FastFloat = inputs[1].get();
if (property1=="Deg")
angle *= toRAD;
value.fromAxisAngle(vec, angle);
}
}
case "EulerAngles": {
if (inputs[0] != null){
var vec: Vec4 = new Vec4().setFrom(inputs[0].get());
if (property1=="Deg"){
vec.x *= toRAD;
vec.y *= toRAD;
vec.z *= toRAD;
}
this.value.fromEulerOrdered(vec, property2);
}
}
default: {
return property0;
}
}
return this.value;
}
override function set(value: Dynamic) {
switch (property0){
case "Quaternion": {
if (input_length>1) {
var vect = new Vec4();
vect.x = value.x;
vect.y = value.y;
vect.z = value.z;
inputs[0].set(vect);
inputs[1].set(value.w);
}
}
case "AxisAngle": {
if (input_length>1){
var vec = new Vec4();
var angle = this.value.toAxisAngle(vec);
if (property1=="Deg")
angle /= toRAD;
inputs[0].set(vec);
inputs[1].set(angle);
}
}
case "EulerAngles": {
if (input_length>0){
var vec:Vec4 = value.toEulerOrdered(property2);
if (property1=="Deg"){
vec.x /= toRAD;
vec.y /= toRAD;
vec.z /= toRAD;
}
inputs[0].set(vec);
}
}
}
if (input_length > 0){
// NYI
}else this.value=value;
}
}

View file

@ -0,0 +1,60 @@
package armory.logicnode;
import kha.FastFloat;
import iron.math.Quat;
import iron.math.Vec4;
class SeparateRotationNode extends LogicNode {
public var property0 = "EulerAngles"; // EulerAngles, AxisAngle, or Quat
public var property1 = "Rad"; // Rad or Deg
public var property2 = "XYZ";
static inline var toDEG:FastFloat = 57.29577951308232; // 180/pi
var input_cache = new Quat();
var euler_cache = new Vec4();
var aa_axis_cache = new Vec4();
var aa_angle_cache: Float = 0;
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var q: Quat = inputs[0].get();
if (q == null) return null;
q.normalize();
switch (property0) {
case "EulerAngles":
if (q!=this.input_cache)
euler_cache = q.toEulerOrdered(property2);
if (from>0)
return null;
switch (property1){
case "Rad": return euler_cache;
case "Deg": return new Vec4(euler_cache.x*toDEG, euler_cache.y*toDEG, euler_cache.z*toDEG);
}
case "AxisAngle":
if (q!=this.input_cache)
aa_angle_cache = q.toAxisAngle(aa_axis_cache);
switch (from){
case 0: return aa_axis_cache;
case 1: switch(property1){
case "Rad": return aa_angle_cache;
case "Deg": return toDEG*aa_angle_cache;
}
}
case "Quaternion":
switch(from){
case 0: return new Vec4(q.x,q.y,q.z);
case 1: return q.w;
}
}
return null;
}
}

View file

@ -20,7 +20,7 @@ class SeparateTransformNode extends LogicNode {
matrix.decompose(loc, rot, scale);
if (from == 0) return loc;
else if (from == 1) return rot.getEuler();
else if (from == 1) return rot;
else return scale;
}
}

View file

@ -1,13 +1,13 @@
package armory.logicnode;
import iron.object.Object;
import iron.math.Quat;
import iron.math.Vec4;
import armory.trait.physics.RigidBody;
class SetRotationNode extends LogicNode {
public var property0: String;
public var property0: String; // UNUSED
public function new(tree: LogicTree) {
super(tree);
@ -16,27 +16,13 @@ class SetRotationNode extends LogicNode {
override function run(from: Int) {
var object: Object = inputs[1].get();
if (object == null) return;
var vec: Vec4 = inputs[2].get();
if (vec == null) return;
var w: Float = inputs[3].get();
var q: Quat = inputs[2].get();
if (q == null) return;
switch (property0) {
case "Euler Angles":
object.transform.rot.fromEuler(vec.x, vec.y, vec.z);
case "Angle Axies (Degrees)" | "Angle Axies (Radians)":
var angle: Float = w;
if (property0 == "Angle Axies (Degrees)") {
angle = angle * (Math.PI / 180);
}
var angleSin = Math.sin(angle / 2);
vec = vec.normalize();
var angleCos = Math.cos(angle / 2);
object.transform.rot = new Quat(vec.x * angleSin, vec.y * angleSin, vec.z * angleSin, angleCos);
case "Quaternion":
object.transform.rot = new Quat(vec.x, vec.y, vec.z, w);
object.transform.rot.normalize();
}
q.normalize();
object.transform.rot = q;
object.transform.buildMatrix();
#if arm_physics
var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) {

View file

@ -7,9 +7,9 @@ import iron.math.Quat;
class TransformNode extends LogicNode {
var value: Mat4 = Mat4.identity();
static var q = new Quat();
static var v1 = new Vec4();
static var v2 = new Vec4();
var q = new Quat();
var v1 = new Vec4();
var v2 = new Vec4();
public function new(tree: LogicTree) {
super(tree);
@ -17,18 +17,21 @@ class TransformNode extends LogicNode {
override function get(from: Int): Dynamic {
var loc: Vec4 = inputs[0].get();
var rot: Vec4 = inputs[1].get();
var rot: Quat = new Quat().setFrom(inputs[1].get());
rot.normalize();
var scale: Vec4 = inputs[2].get();
if (loc == null && rot == null && scale == null) return this.value;
if (loc == null || rot == null || scale == null) return null;
q.fromEuler(rot.x, rot.y, rot.z);
value.compose(loc, q, scale);
return value;
this.value.compose(loc, rot, scale);
return this.value;
}
override function set(value: Dynamic) {
cast(value, Mat4).decompose(v1, q, v2);
inputs[0].set(v1);
inputs[1].set(q.getEuler());
inputs[2].set(v2);
if (inputs.length>0){
cast(value, Mat4).decompose(v1, q, v2);
inputs[0].set(v1);
inputs[1].set(q);
inputs[2].set(v2);
}else this.value = value;
}
}

View file

@ -6,6 +6,7 @@ from typing import OrderedDict as ODict # Prevent naming conflicts
import bpy.types
from bpy.props import *
from nodeitems_utils import NodeItem
from arm.logicnode.arm_sockets import ArmCustomSocket
import arm # we cannot import arm.livepatch here or we have a circular import
# Pass custom property types and NodeReplacement forward to individual
@ -169,7 +170,13 @@ class ArmLogicTreeNode(bpy.types.Node):
socket = self.inputs.new(socket_type, socket_name)
if default_value is not None:
socket.default_value = default_value
if isinstance(socket, ArmCustomSocket):
if socket.arm_socket_type != 'NONE':
socket.default_value_raw = default_value
else:
raise ValueError('specified a default value for an input node that doesn\'t accept one')
else: # should not happen anymore?
socket.default_value = default_value
if is_var and not socket.display_shape.endswith('_DOT'):
socket.display_shape += '_DOT'
@ -185,8 +192,12 @@ class ArmLogicTreeNode(bpy.types.Node):
"""
socket = self.outputs.new(socket_type, socket_name)
# FIXME: …a default_value on an output socket? Why is that a thing?
if default_value is not None:
socket.default_value = default_value
if socket.arm_socket_type != 'NONE':
socket.default_value_raw = default_value
else:
raise ValueError('specified a default value for an input node that doesn\'t accept one')
if is_var and not socket.display_shape.endswith('_DOT'):
socket.display_shape += '_DOT'
@ -208,7 +219,14 @@ class ArmNodeAddInputButton(bpy.types.Operator):
def execute(self, context):
global array_nodes
inps = array_nodes[self.node_index].inputs
inps.new(self.socket_type, self.name_format.format(str(len(inps) + self.index_name_offset)))
socket_types = self.socket_type.split(';')
name_formats = self.name_format.split(';')
assert len(socket_types)==len(name_formats)
format_index = (len(inps) + self.index_name_offset) //len(socket_types)
for socket_type, name_format in zip(socket_types, name_formats):
inps.new(socket_type, name_format.format(str(format_index)))
# Reset to default again for subsequent calls of this operator
self.node_index = ''
@ -238,14 +256,17 @@ class ArmNodeRemoveInputButton(bpy.types.Operator):
bl_label = 'Remove Input'
bl_options = {'UNDO', 'INTERNAL'}
node_index: StringProperty(name='Node Index', default='')
count: IntProperty(name='Number of inputs to remove', default=1, min=1)
min_inputs: IntProperty(name='Number of inputs to keep', default=0, min=0)
def execute(self, context):
global array_nodes
node = array_nodes[self.node_index]
inps = node.inputs
min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs
if len(inps) > min_inps:
inps.remove(inps.values()[-1])
min_inps = self.min_inputs if not hasattr(node, 'min_inputs') else node.min_inputs
if len(inps) >= min_inps + self.count:
for _ in range(self.count):
inps.remove(inps.values()[-1])
return{'FINISHED'}
class ArmNodeRemoveInputValueButton(bpy.types.Operator):
@ -254,13 +275,14 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator):
bl_label = 'Remove Input'
bl_options = {'UNDO', 'INTERNAL'}
node_index: StringProperty(name='Node Index', default='')
target_name: StringProperty(name='Name of socket to remove', default='Value')
def execute(self, context):
global array_nodes
node = array_nodes[self.node_index]
inps = node.inputs
min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs
if len(inps) > min_inps and inps[-1].name == 'Value':
if len(inps) > min_inps and inps[-1].name == self.target_name:
inps.remove(inps.values()[-1])
return{'FINISHED'}
@ -278,7 +300,14 @@ class ArmNodeAddOutputButton(bpy.types.Operator):
def execute(self, context):
global array_nodes
outs = array_nodes[self.node_index].outputs
outs.new(self.socket_type, self.name_format.format(str(len(outs) + self.index_name_offset)))
socket_types = self.socket_type.split(';')
name_formats = self.name_format.split(';')
assert len(socket_types)==len(name_formats)
format_index = (len(outs) + self.index_name_offset) //len(socket_types)
for socket_type, name_format in zip(socket_types, name_formats):
outs.new(socket_type, name_format.format(str(format_index)))
# Reset to default again for subsequent calls of this operator
self.node_index = ''
@ -294,14 +323,16 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator):
bl_label = 'Remove Output'
bl_options = {'UNDO', 'INTERNAL'}
node_index: StringProperty(name='Node Index', default='')
count: IntProperty(name='Number of outputs to remove', default=1, min=1)
def execute(self, context):
global array_nodes
node = array_nodes[self.node_index]
outs = node.outputs
min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs
if len(outs) > min_outs:
outs.remove(outs.values()[-1])
if len(outs) >= min_outs + self.count:
for _ in range(self.count):
outs.remove(outs.values()[-1])
return{'FINISHED'}
class ArmNodeAddInputOutputButton(bpy.types.Operator):
@ -322,8 +353,21 @@ class ArmNodeAddInputOutputButton(bpy.types.Operator):
node = array_nodes[self.node_index]
inps = node.inputs
outs = node.outputs
inps.new(self.in_socket_type, self.in_name_format.format(str(len(inps) + self.in_index_name_offset)))
outs.new(self.out_socket_type, self.out_name_format.format(str(len(outs))))
in_socket_types = self.in_socket_type.split(';')
in_name_formats = self.in_name_format.split(';')
assert len(in_socket_types)==len(in_name_formats)
out_socket_types = self.out_socket_type.split(';')
out_name_formats = self.out_name_format.split(';')
assert len(out_socket_types)==len(out_name_formats)
in_format_index = (len(outs) + self.index_name_offset) // len(in_socket_types)
out_format_index = len(outs) // len(out_socket_types)
for socket_type, name_format in zip(in_socket_types, in_name_formats):
inps.new(socket_type, name_format.format(str(in_format_index)))
for socket_type, name_format in zip(out_socket_types, out_name_formats):
outs.new(socket_type, name_format.format(str(out_format_index)))
# Reset to default again for subsequent calls of this operator
self.node_index = ''
@ -341,7 +385,9 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator):
bl_label = 'Remove Input Output'
bl_options = {'UNDO', 'INTERNAL'}
node_index: StringProperty(name='Node Index', default='')
in_count: IntProperty(name='Number of inputs to remove', default=1, min=1)
out_count: IntProperty(name='Number of inputs to remove', default=1, min=1)
def execute(self, context):
global array_nodes
node = array_nodes[self.node_index]
@ -349,10 +395,12 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator):
outs = node.outputs
min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs
min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs
if len(inps) > min_inps:
inps.remove(inps.values()[-1])
if len(outs) > min_outs:
outs.remove(outs.values()[-1])
if len(inps) >= min_inps + self.in_count:
for _ in range(self.in_count):
inps.remove(inps.values()[-1])
if len(outs) >= min_outs + self.out_count:
for _ in range(self.out_count):
outs.remove(outs.values()[-1])
return{'FINISHED'}
@ -547,7 +595,10 @@ def deprecated(*alternatives: str, message=""):
def wrapper(cls: ArmLogicTreeNode) -> ArmLogicTreeNode:
cls.bl_label += ' (Deprecated)'
cls.bl_description = f'Deprecated. {cls.bl_description}'
if hasattr(cls, 'bl_description'):
cls.bl_description = f'Deprecated. {cls.bl_description}'
else:
cls.bl_description = 'Deprecated.'
cls.bl_icon = 'ERROR'
cls.arm_is_obsolete = True

View file

@ -1,6 +1,8 @@
from math import pi, cos, sin, sqrt
import bpy
from bpy.props import *
from bpy.types import NodeSocket
import mathutils
import arm.utils
@ -77,6 +79,190 @@ class ArmAnimActionSocket(ArmCustomSocket):
def draw_color(self, context, node):
return 0.8, 0.8, 0.8, 1
class ArmRotationSocket(ArmCustomSocket):
bl_idname = 'ArmRotationSocket'
bl_label = 'Rotation Socket'
arm_socket_type = 'ROTATION' # the internal representation is a quaternion, AKA a '4D vector' (using mathutils.Vector((x,y,z,w)))
def get_default_value(self):
if self.default_value_raw is None:
return mathutils.Vector((0.0,0.0,0.0,1.0))
else:
return self.default_value_raw
def on_unit_update(self, context):
if self.default_value_unit == 'Rad':
fac = pi/180 # deg->rad conversion
else:
fac = 180/pi # rad->deg conversion
if self.default_value_mode == 'AxisAngle':
self.default_value_s3 *= fac
elif self.default_value_mode == 'EulerAngles':
self.default_value_s0 *= fac
self.default_value_s1 *= fac
self.default_value_s2 *= fac
self.do_update_raw(context)
def on_mode_update(self, context):
if self.default_value_mode == 'Quaternion':
summ = abs(self.default_value_s0)
summ+= abs(self.default_value_s1)
summ+= abs(self.default_value_s2)
summ+= abs(self.default_value_s3)
if summ<0.01:
self.default_value_s3 = 1.0
elif self.default_value_mode == 'AxisAngle':
summ = abs(self.default_value_s0)
summ+= abs(self.default_value_s1)
summ+= abs(self.default_value_s2)
if summ<1E-5:
self.default_value_s3 = 0.0
self.do_update_raw(context)
@staticmethod
def convert_to_quaternion(part1,part2,param1,param2,param3):
"""converts a representation of rotation into a quaternion.
``part1`` is a vector, ``part2`` is a scalar or None,
``param1`` is in ('Quaternion', 'EulerAngles', 'AxisAngle'),
``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle,
``param3`` is a len-3 string like "XYZ", for EulerAngles """
if param1=='Quaternion':
qx, qy, qz = part1[0], part1[1], part1[2]
qw = part2
# need to normalize the quaternion for a rotation (having it be 0 is not an option)
ql = sqrt(qx**2+qy**2+qz**2+qw**2)
if abs(ql)<1E-5:
qx, qy, qz, qw = 0.0,0.0,0.0,1.0
else:
qx /= ql
qy /= ql
qz /= ql
qw /= ql
return mathutils.Vector((qx,qy,qz,qw))
elif param1 == 'AxisAngle':
if param2 == 'Deg':
angle = part2 * pi/180
else:
angle = part2
cang, sang = cos(angle/2), sin(angle/2)
x,y,z = part1[0], part1[1], part1[2]
veclen = sqrt(x**2+y**2+z**2)
if veclen<1E-5:
return mathutils.Vector((0.0,0.0,0.0,1.0))
else:
return mathutils.Vector((
x/veclen * sang,
y/veclen * sang,
z/veclen * sang,
cang
))
else: # param1 == 'EulerAngles'
x,y,z = part1[0], part1[1], part1[2]
if param2 == 'Deg':
x *= pi/180
y *= pi/180
z *= pi/180
cx, sx = cos(x/2), sin(x/2)
cy, sy = cos(y/2), sin(y/2)
cz, sz = cos(z/2), sin(z/2)
qw, qx, qy, qz = 1.0,0.0,0.0,0.0
for direction in param3[::-1]:
qwi, qxi,qyi,qzi = {'X': (cx,sx,0,0), 'Y': (cy,0,sy,0), 'Z': (cz,0,0,sz)}[direction]
qw = qw*qwi -qx*qxi -qy*qyi -qz*qzi
qx = qx*qwi +qw*qxi +qy*qzi -qz*qyi
qy = qy*qwi +qw*qyi +qz*qxi -qx*qzi
qz = qz*qwi +qw*qzi +qx*qyi -qy*qxi
return mathutils.Vector((qx,qy,qz,qw))
def do_update_raw(self, context):
part1 = mathutils.Vector((
self.default_value_s0,
self.default_value_s1,
self.default_value_s2, 1
))
part2 = self.default_value_s3
self.default_value_raw = self.convert_to_quaternion(
part1,
self.default_value_s3,
self.default_value_mode,
self.default_value_unit,
self.default_value_order
)
def draw(self, context, layout, node, text):
if (self.is_output or self.is_linked):
layout.label(text=self.name)
else:
coll1 = layout.column(align=True)
coll1.label(text=self.name)
bx=coll1.box()
coll = bx.column(align=True)
coll.prop(self, 'default_value_mode')
if self.default_value_mode in ('EulerAngles', 'AxisAngle'):
coll.prop(self, 'default_value_unit')
if self.default_value_mode == 'EulerAngles':
coll.prop(self, 'default_value_order')
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
elif self.default_value_mode == 'Quaternion':
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
coll.prop(self, 'default_value_s3', text='W')
elif self.default_value_mode == 'AxisAngle':
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
coll.separator()
coll.prop(self, 'default_value_s3', text='Angle')
def draw_color(self, context, node):
return 0.68, 0.22, 0.62, 1
default_value_mode: EnumProperty(
items=[('EulerAngles', 'Euler Angles', 'Euler Angles'),
('AxisAngle', 'Axis/Angle', 'Axis/Angle'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='EulerAngles',
update=on_mode_update)
default_value_unit: EnumProperty(
items=[('Deg', 'Degrees', 'Degrees'),
('Rad', 'Radians', 'Radians')],
name='', default='Rad',
update=on_unit_update)
default_value_order: EnumProperty(
items=[('XYZ','XYZ','XYZ'),
('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'),
('YXZ','YXZ','YXZ'),
('YZX','YZX','YZX'),
('ZXY','ZXY','ZXY'),
('ZYX','ZYX','ZYX')],
name='', default='XYZ'
)
default_value_s0: FloatProperty(update=do_update_raw)
default_value_s1: FloatProperty(update=do_update_raw)
default_value_s2: FloatProperty(update=do_update_raw)
default_value_s3: FloatProperty(update=do_update_raw)
default_value_raw: FloatVectorProperty(
name='Value',
description='Raw quaternion obtained for the default value of a ArmRotationSocket socket',
size=4, default=(0,0,0,1),
update = _on_update_socket
)
class ArmArraySocket(ArmCustomSocket):
bl_idname = 'ArmNodeSocketArray'
@ -226,6 +412,7 @@ class ArmObjectSocket(ArmCustomSocket):
return 0.15, 0.55, 0.75, 1
class ArmStringSocket(ArmCustomSocket):
bl_idname = 'ArmStringSocket'
bl_label = 'String Socket'
@ -292,10 +479,10 @@ def draw_socket_layout_split(socket: bpy.types.NodeSocket, layout: bpy.types.UIL
if not socket.is_output and not socket.is_linked:
layout.prop(socket, prop_name, text='')
REG_CLASSES = (
ArmActionSocket,
ArmAnimActionSocket,
ArmRotationSocket,
ArmArraySocket,
ArmBoolSocket,
ArmColorSocket,

View file

@ -0,0 +1,75 @@
from arm.logicnode.arm_nodes import *
from mathutils import Vector
@deprecated(message='Do not use quaternion sockets')
class QuaternionNode(ArmLogicTreeNode):
"""TO DO."""
bl_idname = 'LNQuaternionNode'
bl_label = 'Quaternion'
bl_description = 'Create a quaternion variable (transported through a vector socket)'
arm_section = 'quaternions'
arm_version = 2 # deprecate
def arm_init(self, context):
self.add_input('ArmFloatSocket', 'X')
self.add_input('ArmFloatSocket', 'Y')
self.add_input('ArmFloatSocket', 'Z')
self.add_input('ArmFloatSocket', 'W', default_value=1.0)
self.add_output('ArmVectorSocket', 'Quaternion')
self.add_output('ArmVectorSocket', 'XYZ')
self.add_output('ArmVectorSocket', 'W')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2[deprecated]
newnodes = []
rawlinks = self.outputs[0].links
xyzlinks = self.outputs[1].links
wlinks = self.outputs[2].links
if len(rawlinks)>0 or len(xyzlinks)>0:
xyzcomb = node_tree.nodes.new('LNVectorNode')
newnodes.append(xyzcomb)
xyzcomb.inputs[0].default_value = self.inputs[0].default_value
xyzcomb.inputs[1].default_value = self.inputs[1].default_value
xyzcomb.inputs[2].default_value = self.inputs[2].default_value
for link in self.inputs[0].links:
node_tree.links.new(link.from_socket, xyzcomb.inputs[0])
for link in self.inputs[1].links:
node_tree.links.new(link.from_socket, xyzcomb.inputs[1])
for link in self.inputs[2].links:
node_tree.links.new(link.from_socket, xyzcomb.inputs[2])
for link in xyzlinks:
node_tree.links.new(xyzcomb.outputs[0], link.to_socket)
if len(rawlinks)>0:
rotnode = node_tree.nodes.new('LNRotationNode')
newnodes.append(rotnode)
rotnode.property0 = 'Quaternion'
rotnode.inputs[0].default_value = Vector(
(self.inputs[0].default_value,
self.inputs[1].default_value,
self.inputs[2].default_value))
rotnode.inputs[1].default_value = self.inputs[3].default_value
node_tree.links.new(xyzcomb.outputs[0], rotnode.inputs[0])
for link in self.inputs[3].links: # 0 or 1
node_tree.links.new(link.from_socket, rotnode.inputs[1])
for link in rawlinks:
node_tree.links.new(rotnode.outputs[0], link.to_socket)
if len(self.inputs[3].links)>0:
fromval = self.inputs[3].links[0].from_socket
for link in self.outputs[2].links:
node_tree.links.new(fromval, link.to_socket)
else:
for link in self.outputs[2].links:
link.to_socket.default_value = self.inputs[3].default_value
return newnodes

View file

@ -0,0 +1,45 @@
from arm.logicnode.arm_nodes import *
@deprecated(message='Do not use quaternion sockets')
class SeparateQuaternionNode(ArmLogicTreeNode):
"""Splits the given quaternion into X, Y, Z and W."""
bl_idname = 'LNSeparateQuaternionNode'
bl_label = "Separate Quaternion (do not use: quaternions sockets have been phased out entirely)"
bl_description = "Separate a quaternion object (transported through a vector socket) into its four compoents."
arm_section = 'quaternions'
arm_version = 2 # deprecate
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Quaternion')
self.add_output('ArmFloatSocket', 'X')
self.add_output('ArmFloatSocket', 'Y')
self.add_output('ArmFloatSocket', 'Z')
self.add_output('ArmFloatSocket', 'W')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2[deprecated]
newself = node_tree.nodes.new('LNSeparateRotationNode')
separator = node_tree.nodes.new('LNSeparateVectorNode')
newself.property0 = 'Quaternion'
newself.property1 = 'Rad' # bogus
newself.property2 = 'XYZ' # bogus
for link in self.inputs[0].links:
node_tree.links.new(link.from_socket, newself.inputs[0])
node_tree.links.new(newself.outputs[0], separator.inputs[0])
for link in self.outputs[0].links:
node_tree.links.new(separator.outputs[0], link.to_socket)
for link in self.outputs[1].links:
node_tree.links.new(separator.outputs[1], link.to_socket)
for link in self.outputs[2].links:
node_tree.links.new(separator.outputs[2], link.to_socket)
for link in self.outputs[3].links:
node_tree.links.new(newself.outputs[1], link.to_socket)
return [newself, separator]

View file

@ -1,47 +1,52 @@
from arm.logicnode.arm_nodes import *
from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation
class QuaternionMathNode(ArmLogicTreeNode):
"""Mathematical operations on quaternions."""
bl_idname = 'LNQuaternionMathNode'
bl_label = 'Quaternion Math'
bl_description = 'Mathematical operations that can be performed on rotations, when represented as quaternions specifically'
arm_section = 'quaternions'
arm_version = 1
arm_version = 2
def get_bool(self):
return self.get('property1', False)
def set_bool(self, value):
self['property1'] = value
if value:
if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1):
self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle
self.add_output('ArmFloatSocket', 'X') # Result X
self.add_output('ArmFloatSocket', 'Y') # Result Y
self.add_output('ArmFloatSocket', 'Z') # Result Z
self.add_output('ArmFloatSocket', 'W') # Result W
if (self.property0 == 'Module'):
self.add_output('ArmFloatSocket', 'Module') # Module
if (self.property0 == 'DotProduct'):
self.add_output('ArmFloatSocket', 'Scalar') # DotProduct
if (self.property0 == 'ToAxisAngle'):
self.add_output('ArmFloatSocket', 'To Axis Angle') # ToAxisAngle
def ensure_input_socket(self, socket_number, newclass, newname, default_value=None):
while len(self.inputs) < socket_number:
self.inputs.new('ArmFloatSocket', 'BOGUS')
if len(self.inputs) > socket_number:
if len(self.inputs[socket_number].links) == 1:
source_socket = self.inputs[socket_number].links[0].from_socket
else:
source_socket = None
if (
self.inputs[socket_number].bl_idname == newclass \
and self.inputs[socket_number].arm_socket_type != 'NONE'
):
default_value = self.inputs[socket_number].default_value_raw
self.inputs.remove(self.inputs[socket_number])
else:
if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1):
self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle
# Remove X, Y, Z, W
for i in range(4):
if len(self.outputs) > 1:
self.outputs.remove(self.outputs.values()[-1])
else:
break
if (self.property0 == 'Module'):
self.add_output('ArmFloatSocket', 'Module') # Module
if (self.property0 == 'DotProduct'):
self.add_output('ArmFloatSocket', 'Scalar') # DotProduct
if (self.property0 == 'ToAxisAngle'):
self.add_output('ArmFloatSocket', 'To Axis Angle') # ToAxisAngle
source_socket = None
self.inputs.new(newclass, newname)
if default_value != None:
self.inputs[-1].default_value_raw = default_value
self.inputs.move(len(self.inputs)-1, socket_number)
if source_socket is not None:
self.id_data.links.new(source_socket, self.inputs[socket_number])
def ensure_output_socket(self, socket_number, newclass, newname):
sink_sockets = []
while len(self.outputs) < socket_number:
self.outputs.new('ArmFloatSocket', 'BOGUS')
if len(self.outputs) > socket_number:
for link in self.inputs[socket_number].links:
sink_sockets.append(link.to_socket)
self.inputs.remove(self.inputs[socket_number])
property1: HaxeBoolProperty('property1', name='Separator Out', default=False, set=set_bool, get=get_bool)
self.inputs.new(newclass, newname)
self.inputs.move(len(self.inputs)-1, socket_number)
for socket in sink_sockets:
self.id_data.links.new(self.inputs[socket_number], socket)
@staticmethod
def get_enum_id_value(obj, prop_name, value):
@ -75,63 +80,43 @@ class QuaternionMathNode(ArmLogicTreeNode):
# Checking the selection of another operation
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
if select_prev != select_current:
# Remove
count = 0
if ((select_prev == 'Add') or (select_prev == 'Subtract') or (select_prev == 'Multiply') or (select_prev == 'DotProduct')) and ((select_current == 'Add') or (select_current == 'Subtract') or (select_current == 'Multiply') or (select_current == 'DotProduct')) or (((select_current == 'Lerp') or (select_current == 'Slerp')) and ((select_prev == 'Lerp') or (select_prev == 'Slerp'))):
count = len(self.inputs)
while (len(self.inputs) > count):
self.inputs.remove(self.inputs.values()[-1])
if (select_prev == 'DotProduct') or (select_prev == 'ToAxisAngle') or (select_prev == 'Module'):
self.outputs.remove(self.outputs.values()[-1])
# Many arguments: Add, Subtract, DotProduct, Multiply, MultiplyFloat
if (self.get_count_in(select_current) == 0):
if (select_current == "MultiplyFloats"):
self.add_input('ArmVectorSocket', 'Quaternion ' + str(len(self.inputs)))
self.add_input('ArmFloatSocket', 'Value ' + str(len(self.inputs)))
else:
while (len(self.inputs) < 2):
self.add_input('ArmVectorSocket', 'Quaternion ' + str(len(self.inputs)))
if (select_current == 'DotProduct'):
self.add_output('ArmFloatSocket', 'Scalar')
if select_current in ('Add','Subtract','Multiply','DotProduct') \
and select_prev in ('Add','Subtract','Multiply','DotProduct'):
pass # same as select_current==select_prev for the sockets
elif select_prev != select_current:
if select_current in ('Add','Subtract','Multiply','DotProduct'):
for i in range( max(len(self.inputs)//2 ,2) ):
self.ensure_input_socket(2*i, 'ArmVectorSocket', 'Quaternion %d XYZ'%i)
self.ensure_input_socket(2*i+1, 'ArmFloatSocket', 'Quaternion %d W'%i, default_value=1.0)
if len(self.inputs)%1:
self.inputs.remove(self.inputs[len(self.inputs)-1])
elif select_current == 'MultiplyFloats':
self.ensure_input_socket(0, 'ArmVectorSocket', 'Quaternion XYZ')
self.ensure_input_socket(1, 'ArmFloatSocket', 'Quaternion W', default_value=1.0)
for i in range( max(len(self.inputs)-2 ,1) ):
self.ensure_input_socket(i+2, 'ArmFloatSocket', 'Value %d'%i)
elif select_current in ('Module', 'Normalize'):
self.ensure_input_socket(0, 'ArmVectorSocket', 'Quaternion XYZ')
self.ensure_input_socket(1, 'ArmFloatSocket', 'Quaternion W', default_value=1.0)
while len(self.inputs)>2:
self.inputs.remove(self.inputs[2])
else:
raise ValueError('Internal code of LNQuaternionMathNode failed to deal correctly with math operation "%s". Please report this to the developers.' %select_current)
# 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler
if (self.get_count_in(select_current) == 3):
if (select_current == 'Lerp') or (select_current == 'Slerp'):
while (len(self.inputs) < 3):
self.add_input('ArmVectorSocket', 'From')
self.add_input('ArmVectorSocket', 'To')
self.add_input('ArmFloatSocket', 'T')
if (select_current == 'FromAxisAngle'):
self.add_input('ArmVectorSocket', 'Quaternion')
self.add_input('ArmVectorSocket', 'Axis')
self.add_input('ArmFloatSocket', 'Angle')
if (select_current == 'FromEuler'):
self.add_input('ArmFloatSocket', 'X')
self.add_input('ArmFloatSocket', 'Y')
self.add_input('ArmFloatSocket', 'Z')
if select_current in ('Add','Subtract','Multiply','MultiplyFloats','Normalize'):
self.outputs[0].name = 'XYZ Out'
self.outputs[1].name = 'W Out'
else:
self.outputs[0].name = '[unused]'
self.outputs[1].name = 'Value Out'
# 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle
if (self.get_count_in(select_current) == 2):
if (select_current == 'FromTo'):
self.add_input('ArmVectorSocket', 'Vector ' + str(len(self.inputs)))
self.add_input('ArmVectorSocket', 'Vector ' + str(len(self.inputs)))
if (select_current == 'FromMat') or (select_current == 'FromRotationMat'):
self.add_input('ArmVectorSocket', 'Quaternion')
self.add_input('ArmDynamicSocket', 'Matrix')
if (select_current == 'ToAxisAngle'):
self.add_input('ArmVectorSocket', 'Quaternion')
self.add_input('ArmVectorSocket', 'Axis')
self.add_output('ArmFloatSocket', 'Angle')
# 1 argument: Module, Normalize, GetEuler
if (self.get_count_in(select_current) == 1):
self.add_input('ArmVectorSocket', 'Quaternion')
if (select_current == 'Module'):
self.add_output('ArmFloatSocket', 'Module')
self['property0'] = value
self['property0_proxy'] = value
# this property swaperoo is kinda janky-looking, but necessary.
# Read more on LN_rotate_object.py
property0: HaxeEnumProperty(
'property0',
items = [('Add', 'Add', 'Add'),
@ -140,29 +125,44 @@ class QuaternionMathNode(ArmLogicTreeNode):
('Multiply', 'Multiply', 'Multiply'),
('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'),
('Module', 'Module', 'Module'),
('Normalize', 'Normalize', 'Normalize'),
('Lerp', 'Lerp', 'Linearly interpolate'),
('Slerp', 'Slerp', 'Spherical linear interpolation'),
('FromTo', 'From To', 'From To'),
('FromMat', 'From Mat', 'From Mat'),
('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat'),
('ToAxisAngle', 'To Axis Angle', 'To Axis Angle'),
('FromAxisAngle', 'From Axis Angle', 'From Axis Angle'),
('FromEuler', 'From Euler', 'From Euler'),
('GetEuler', 'To Euler', 'To Euler')],
('Normalize', 'Normalize', 'Normalize'), #],
# NOTE: the unused parts need to exist to be read from an old version from the node.
# this is so dumb…
('Lerp', 'DO NOT USE',''),
('Slerp', 'DO NOT USE',''),
('FromTo', 'DO NOT USE',''),
('FromMat', 'DO NOT USE',''),
('FromRotationMat', 'DO NOT USE',''),
('ToAxisAngle', 'DO NOT USE',''),
('FromAxisAngle', 'DO NOT USE',''),
('FromEuler', 'DO NOT USE',''),
('GetEuler', 'DO NOT USE','')],
name='', default='Add') #, set=set_enum, get=get_enum)
property0_proxy: EnumProperty(
items = [('Add', 'Add', 'Add'),
('Subtract', 'Subtract', 'Subtract'),
('DotProduct', 'Dot Product', 'Dot Product'),
('Multiply', 'Multiply', 'Multiply'),
('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'),
('Module', 'Module', 'Module'),
('Normalize', 'Normalize', 'Normalize')],
name='', default='Add', set=set_enum, get=get_enum)
def __init__(self):
super(QuaternionMathNode, self).__init__()
array_nodes[str(id(self))] = self
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Quaternion 0', default_value=[0.0, 0.0, 0.0])
self.add_input('ArmVectorSocket', 'Quaternion 1', default_value=[0.0, 0.0, 0.0])
self.add_output('ArmVectorSocket', 'Result')
self.add_input('ArmVectorSocket', 'Quaternion 0 XYZ', default_value=[0.0, 0.0, 0.0])
self.add_input('ArmFloatSocket', 'Quaternion 0 W', default_value=1)
self.add_input('ArmVectorSocket', 'Quaternion 1 XYZ', default_value=[0.0, 0.0, 0.0])
self.add_input('ArmFloatSocket', 'Quaternion 1 W', default_value=1)
self.add_output('ArmVectorSocket', 'Result XYZ', default_value=[0.0, 0.0, 0.0])
self.add_output('ArmFloatSocket', 'Result W', default_value=1)
def draw_buttons(self, context, layout):
layout.prop(self, 'property1') # Separator Out
layout.prop(self, 'property0') # Operation
layout.prop(self, 'property0_proxy') # Operation
# Buttons
if (self.get_count_in(self.property0) == 0):
row = layout.row(align=True)
@ -170,15 +170,210 @@ class QuaternionMathNode(ArmLogicTreeNode):
op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True)
op.node_index = str(id(self))
if (self.property0 == 'Add') or (self.property0 == 'Subtract') or (self.property0 == 'Multiply') or (self.property0 == 'DotProduct'):
op.name_format = 'Quaternion {0}'
op.name_format = 'Quaternion {0} XYZ;Quaternion {0} W'
else:
op.name_format = 'Value {0}'
if (self.property0 == "MultiplyFloats"):
op.socket_type = 'ArmFloatSocket'
else:
op.socket_type = 'ArmVectorSocket'
op.socket_type = 'ArmVectorSocket;ArmFloatSocket'
column = row.column(align=True)
op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True)
op.node_index = str(id(self))
if self.property0 != "MultiplyFloats":
op.count = 2
op.min_inputs = 4
else:
op.min_inputs = 2
if len(self.inputs) == 2:
column.enabled = False
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
ret=[]
if self.property0 == 'GetEuler':
newself = node_tree.nodes.new('LNSeparateRotationNode')
ret.append(newself)
newself.property0='EulerAngles'
newself.property2='XZY'
newself.property1='Rad'
for link in self.inputs[0].links: # 0 or 1
node_tree.links.new(link.from_socket, newself.inputs[0])
elif self.property0 == 'FromEuler':
newself = node_tree.nodes.new('LNRotationNode')
ret.append(newself)
preconv = node_tree.nodes.new('LNVectorNode')
ret.append(preconv)
newself.property0='EulerAngles'
newself.property2='XZY'
newself.property1='Rad'
node_tree.links.new(preconv.outputs[0], newself.inputs[0])
preconv.inputs[0].default_value = self.inputs[0].default_value
for link in self.inputs[0].links: # 0 or 1
node_tree.links.new(link.from_socket, preconv.inputs[0])
preconv.inputs[1].default_value = self.inputs[1].default_value
for link in self.inputs[1].links: # 0 or 1
node_tree.links.new(link.from_socket, preconv.inputs[1])
preconv.inputs[2].default_value = self.inputs[2].default_value
for link in self.inputs[2].links: # 0 or 1
node_tree.links.new(link.from_socket, preconv.inputs[2])
elif self.property0 == 'ToAxisAngle':
newself = node_tree.nodes.new('LNSeparateRotationNode')
ret.append(newself)
newself.property0='AxisAngle'
newself.property1='Rad'
for link in self.inputs[0].links: # 0 or 1
node_tree.links.new(link.from_socket, newself.inputs[0])
elif self.property0 == 'FromAxisAngle':
newself = node_tree.nodes.new('LNRotationNode')
ret.append(newself)
newself.property0='AxisAngle'
newself.property1='Rad'
newself.inputs[0].default_value = self.inputs[1].default_value
for link in self.inputs[1].links: # 0 or 1
node_tree.links.new(link.from_socket, newself.inputs[0])
newself.inputs[1].default_value = self.inputs[2].default_value
for link in self.inputs[2].links: # 0 or 1
node_tree.links.new(link.from_socket, newself.inputs[1])
elif self.property0 in ('FromMat','FromRotationMat'):
newself = node_tree.nodes.new('LNSeparateTransformNode')
ret.append(newself)
for link in self.inputs[1].links: # 0 or 1
node_tree.links.new(link.from_socket, newself.inputs[0])
elif self.property0 in ('Lerp','Slerp','FromTo'):
newself = node_tree.nodes.new('LNRotationMathNode')
ret.append(newself)
newself.property0 = self.property0
for in1, in2 in zip(self.inputs, newself.inputs):
if in2.bl_idname == 'ArmRotationSocket':
in2.default_value_raw = Rotation.convert_to_quaternion(
in1.default_value,0,
'EulerAngles','Rad','XZY'
)
elif in1.bl_idname in ('ArmFloatSocket', 'ArmVectorSocket'):
in2.default_value = in1.default_value
for link in in1.links:
node_tree.links.new(link.from_socket, in2)
else:
newself = node_tree.nodes.new('LNQuaternionMathNode')
ret.append(newself)
newself.property0 = self.property0
# convert the inputs… this is going to be hard lmao.
i_in_1 = 0
i_in_2 = 0
while i_in_1 < len(self.inputs):
in1 = self.inputs[i_in_1]
if in1.bl_idname == 'ArmVectorSocket':
# quaternion input: now two sockets, not one.
convnode = node_tree.nodes.new('LNSeparateRotationNode')
convnode.property0 = 'Quaternion'
ret.append(convnode)
if i_in_2 >= len(newself.inputs):
newself.ensure_input_socket(i_in_2, 'ArmVectorSocket', 'Quaternion %d XYZ'%(i_in_1))
newself.ensure_input_socket(i_in_2+1, 'ArmFloatSocket', 'Quaternion %d W'%(i_in_1), 1.0)
node_tree.links.new(convnode.outputs[0], newself.inputs[i_in_2])
node_tree.links.new(convnode.outputs[1], newself.inputs[i_in_2+1])
for link in in1.links:
node_tree.links.new(link.from_socket, convnode.inputs[0])
i_in_2 +=2
i_in_1 +=1
elif in1.bl_idname == 'ArmFloatSocket':
for link in in1.links:
node_tree.links.new(link.from_socket, newself.inputs[i_in_2])
i_in_1 +=1
i_in_2 +=1
else:
raise ValueError('get_replacement_node() for is not LNQuaternionMathNode V1->V2 is not prepared to deal with an input socket of type %s. This is a bug to report to the developers' %in1.bl_idname)
# #### now that the input has been dealt with, let's deal with the output.
if self.property0 in ('FromEuler','FromMat','FromRotationMat','FromAxisAngle','Lerp','Slerp','FromTo'):
# the new self returns a rotation
for link in self.outputs[0].links:
out_sock_i = int( self.property0.endswith('Mat') )
node_tree.links.new(newself.outputs[out_sock_i], link.to_socket)
elif self.property0 in ('DotProduct','Module'):
# new self returns a float
for link in self.outputs[1 + 4*int(self.property1)].links:
node_tree.links.new(newself.outputs[1], link.to_socket)
elif self.property0 in ('GetEuler', 'ToAxisAngle'):
# new self returns misc.
for link in self.outputs[0].links:
node_tree.links.new(newself.outputs[0], link.to_socket)
if self.property0 == 'ToAxisAngle':
for link in self.outputs[1 + 4*int(self.property1)].links:
node_tree.links.new(newself.outputs[1], link.to_socket)
if self.property1:
xlinks = self.outputs[1].links
ylinks = self.outputs[2].links
zlinks = self.outputs[3].links
if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0:
conv = node_tree.nodes.new('LNSeparateVectorNode')
ret.append(conv)
node_tree.links.new(newself.outputs[0], conv.inputs[0])
for link in xlinks:
node_tree.links.new(conv.outputs[0], link.to_socket)
for link in ylinks:
node_tree.links.new(conv.outputs[1], link.to_socket)
for link in zlinks:
node_tree.links.new(conv.outputs[2], link.to_socket)
else:
# new self returns a proper quaternion XYZ/W
outlinks = self.outputs[0].links
if len(outlinks)>0:
conv = node_tree.nodes.new('LNRotationNode')
conv.property0='Quaternion'
ret.append(conv)
node_tree.links.new(newself.outputs[0], conv.inputs[0])
node_tree.links.new(newself.outputs[1], conv.inputs[1])
for link in outlinks:
node_tree.links.new(conv.outputs[0], link.to_socket)
if self.property1:
for link in self.outputs[4].links: # for W
node_tree.links.new(newself.outputs[1], link.to_socket)
xlinks = self.outputs[1].links
ylinks = self.outputs[2].links
zlinks = self.outputs[3].links
if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0:
conv = node_tree.nodes.new('LNSeparateVectorNode')
ret.append(conv)
node_tree.links.new(newself.outputs[0], conv.inputs[0])
for link in xlinks:
node_tree.links.new(conv.outputs[0], link.to_socket)
for link in ylinks:
node_tree.links.new(conv.outputs[1], link.to_socket)
for link in zlinks:
node_tree.links.new(conv.outputs[2], link.to_socket)
for node in ret: # update the labels on the node's displays
if node.bl_idname == 'LNSeparateRotationNode':
node.on_property_update(None)
elif node.bl_idname == 'LNRotationNode':
node.on_property_update(None)
elif node.bl_idname == 'LNRotationMathNode':
node.on_update_operation(None)
elif node.bl_idname == 'LNQuaternionMathNode':
node.set_enum(node.get_enum())
return ret
# note: keep property1, so that it is actually readable for node conversion.
property1: BoolProperty(name='DEPRECATED', default=False)

View file

@ -0,0 +1,121 @@
from arm.logicnode.arm_nodes import *
from mathutils import Vector
class RotationMathNode(ArmLogicTreeNode):
"""Mathematical operations on rotations."""
bl_idname = 'LNRotationMathNode'
bl_label = 'Rotation Math'
bl_description = 'Mathematical operations that can be performed on rotations, no matter their internal representation'
arm_section = 'quaternions'
arm_version = 1
@staticmethod
def get_count_in(operation_name):
return {
'Inverse': 1,
'Normalize': 1,
'Compose': 2,
'Amplify': 2,
'FromTo': 2,
#'FromRotationMat': 2,
'Lerp': 3,
'Slerp': 3,
}.get(operation_name, 0)
def ensure_input_socket(self, socket_number, newclass, newname):
while len(self.inputs) < socket_number:
self.inputs.new('ArmFloatSocket', 'BOGUS')
if len(self.inputs) > socket_number:
if len(self.inputs[socket_number].links) == 1:
source_socket = self.inputs[socket_number].links[0].from_socket
else:
source_socket = None
self.inputs.remove(self.inputs[socket_number])
else:
source_socket = None
self.inputs.new(newclass, newname)
self.inputs.move(len(self.inputs)-1, socket_number)
if source_socket is not None:
self.id_data.links.new(source_socket, self.inputs[socket_number])
def ensure_output_socket(self, socket_number, newclass, newname):
sink_sockets = []
while len(self.outputs) < socket_number:
self.outputs.new('ArmFloatSocket', 'BOGUS')
if len(self.outputs) > socket_number:
for link in self.inputs[socket_number].links:
sink_sockets.append(link.to_socket)
self.inputs.remove(self.inputs[socket_number])
self.inputs.new(newclass, newname)
self.inputs.move(len(self.inputs)-1, socket_number)
for socket in sink_sockets:
self.id_data.links.new(self.inputs[socket_number], socket)
def on_property_update(self, context):
# Checking the selection of another operation
# Rotation as argument 0:
if self.property0 in ('Inverse','Normalize','Amplify'):
self.ensure_input_socket(0, "ArmRotationSocket", "Rotation")
self.ensure_input_socket(1, "ArmFloatSocket", "Amplification factor")
elif self.property0 in ('Slerp','Lerp','Compose'):
self.ensure_input_socket(0, "ArmRotationSocket", "From")
self.ensure_input_socket(1, "ArmRotationSocket", "To")
if self.property0 == 'Compose':
self.inputs[0].name = 'Outer rotation'
self.inputs[1].name = 'Inner rotation'
else:
self.ensure_input_socket(2, "ArmFloatSocket", "Interpolation factor")
elif self.property0 == 'FromTo':
self.ensure_input_socket(0, "ArmVectorSocket", "From")
self.ensure_input_socket(1, "ArmVectorSocket", "To")
# Rotation as argument 1:
if self.property0 in ('Compose','Lerp','Slerp'):
if self.inputs[1].bl_idname != "ArmRotationSocket":
self.replace_input_socket(1, "ArmRotationSocket", "Rotation 2")
if self.property0 == 'Compose':
self.inputs[1].name = "Inner quaternion"
# Float as argument 1:
if self.property0 == 'Amplify':
if self.inputs[1].bl_idname != 'ArmFloatSocket':
self.replace_input_socket(1, "ArmFloatSocket", "Amplification factor")
# Vector as argument 1:
#if self.property0 == 'FromRotationMat':
# # WHAT??
# pass
while len(self.inputs) > self.get_count_in(self.property0):
self.inputs.remove(self.inputs[len(self.inputs)-1])
property0: HaxeEnumProperty(
'property0',
items = [('Compose', 'Compose (multiply)', 'compose (multiply) two rotations. Note that order of the composition matters.'),
('Amplify', 'Amplify (multiply by float)', 'Amplify or diminish the effect of a rotation'),
#('Normalize', 'Normalize', 'Normalize'),
('Inverse', 'Get Inverse', 'from r, get the rotation r2 so that " r×r2=r2×r= <no rotation>" '),
('Lerp', 'Lerp', 'Linearly interpolation'),
('Slerp', 'Slerp', 'Spherical linear interpolation'),
('FromTo', 'From To', 'From direction To direction'),
#('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat')
],
name='', default='Compose', update=on_property_update)
#def __init__(self):
# array_nodes[str(id(self))] = self
def arm_init(self, context):
self.add_input('ArmRotationSocket', 'Outer rotation', default_value=(0.0, 0.0, 0.0, 1.0) )
self.add_input('ArmRotationSocket', 'Inner rotation', default_value=(0.0, 0.0, 0.0, 1.0) )
self.add_output('ArmRotationSocket', 'Result')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0') # Operation

View file

@ -1,16 +0,0 @@
from arm.logicnode.arm_nodes import *
class SeparateQuaternionNode(ArmLogicTreeNode):
"""Splits the given quaternion into X, Y, Z and W."""
bl_idname = 'LNSeparateQuaternionNode'
bl_label = "Separate Quaternion"
arm_section = 'quaternions'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Quaternion')
self.add_output('ArmFloatSocket', 'X')
self.add_output('ArmFloatSocket', 'Y')
self.add_output('ArmFloatSocket', 'Z')
self.add_output('ArmFloatSocket', 'W')

View file

@ -5,14 +5,87 @@ class GetRotationNode(ArmLogicTreeNode):
bl_idname = 'LNGetRotationNode'
bl_label = 'Get Object Rotation'
arm_section = 'rotation'
arm_version = 1
arm_version = 2
def arm_init(self, context):
self.add_input('ArmNodeSocketObject', 'Object')
self.add_output('ArmRotationSocket', 'Rotation')
self.add_output('ArmVectorSocket', 'Euler Angles')
self.add_output('ArmVectorSocket', 'Vector')
self.add_output('ArmFloatSocket', 'Angle (Radians)')
self.add_output('ArmFloatSocket', 'Angle (Degrees)')
self.add_output('ArmVectorSocket', 'Quaternion XYZ')
self.add_output('ArmFloatSocket', 'Quaternion W')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
property0: HaxeEnumProperty(
'property0',
items = [('Local', 'Local', 'Local'),
('Global', 'Global', 'Global')],
name='', default='Local')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNGetRotationNode')
newself.property0 = 'Local'
newnodes = [newself]
if len(self.outputs[0].links)>0:
# euler (radians) needed
converter = self.id_data.nodes.new('LNSeparateRotationNode')
converter.property0 = "EulerAngles"
converter.property1 = "Rad"
converter.property2 = "XZY"
newnodes.append(converter)
self.id_data.links.new(newself.outputs[0], converter.inputs[0])
for link in self.outputs[0].links:
self.id_data.links.new(converter.outputs[0], link.to_socket)
if len(self.outputs[4].links)>0 or len(self.outputs[5].links)>0:
# quaternion needed
converter = self.id_data.nodes.new('LNSeparateRotationNode')
converter.property0 = "Quaternion"
newnodes.append(converter)
self.id_data.links.new(newself.outputs[0], converter.inputs[0])
for link in self.outputs[4].links:
self.id_data.links.new(converter.outputs[0], link.to_socket)
for link in self.outputs[5].links:
self.id_data.links.new(converter.outputs[1], link.to_socket)
if len(self.outputs[1].links)>0 or len(self.outputs[2].links)>0 or len(self.outputs[3].links)>0:
# axis/angle needed
converter = self.id_data.nodes.new('LNSeparateRotationNode')
converter.property0 = "AxisAngle"
converter.property1 = "Rad"
newnodes.append(converter)
self.id_data.links.new(newself.outputs[0], converter.inputs[0])
for link in self.outputs[1].links:
self.id_data.links.new(converter.outputs[0], link.to_socket)
if len(self.outputs[3].links)==0 and len(self.outputs[2].links)==0:
pass
elif len(self.outputs[3].links)==0:
for link in self.outputs[2].links:
self.id_data.links.new(converter.outputs[1], link.to_socket)
elif len(self.outputs[2].links)==0:
converter.property1 = 'Deg'
for link in self.outputs[3].links:
self.id_data.links.new(converter.outputs[1], link.to_socket)
else:
for link in self.outputs[2].links:
self.id_data.links.new(converter.outputs[1], link.to_socket)
converter = self.id_data.nodes.new('LNSeparateRotationNode')
converter.property0 = "AxisAngle"
converter.property1 = "Deg"
converter.property2 = "XYZ" # bogus
newnodes.append(converter)
self.id_data.links.new(newself.outputs[0], converter.inputs[0])
for link in self.outputs[3].links:
self.id_data.links.new(converter.outputs[1], link.to_socket)
return newnodes

View file

@ -1,11 +1,11 @@
from arm.logicnode.arm_nodes import *
class LookAtNode(ArmLogicTreeNode):
"""Converts the two given coordinates to a quaternion rotation."""
"""Returns *a* rotation that makes something look away from X,Y or Z, and instead look in the 'from->to' direction"""
bl_idname = 'LNLookAtNode'
bl_label = 'Look At'
arm_section = 'rotation'
arm_version = 1
arm_version = 2
property0: HaxeEnumProperty(
'property0',
@ -21,7 +21,27 @@ class LookAtNode(ArmLogicTreeNode):
self.add_input('ArmVectorSocket', 'From Location')
self.add_input('ArmVectorSocket', 'To Location')
self.add_output('ArmVectorSocket', 'Rotation')
self.add_output('ArmRotationSocket', 'Rotation')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNLookAtNode')
converter = self.id_data.nodes.new('LNSeparateRotationNode')
self.id_data.links.new(newself.outputs[0], converter.inputs[0])
converter.property0 = 'EulerAngles'
converter.property1 = 'Rad'
converter.property2 = 'XZY'
for link in self.outputs[0].links:
self.id_data.links.new(converter.outputs[0], link.to_socket)
return [newself, converter]

View file

@ -1,49 +1,105 @@
from arm.logicnode.arm_nodes import *
from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation
class RotateObjectNode(ArmLogicTreeNode):
"""Rotates the given object."""
bl_idname = 'LNRotateObjectNode'
bl_label = 'Rotate Object'
arm_section = 'rotation'
arm_version = 1
arm_version = 2
def arm_init(self, context):
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('ArmVectorSocket', 'Euler Angles')
self.add_input('ArmFloatSocket', 'Angle / W')
self.add_input('ArmRotationSocket', 'Rotation')
self.add_output('ArmNodeSocketAction', 'Out')
def on_property_update(self, context):
""" called by the EnumProperty, used to update the node socket labels"""
if self.property0 == "Quaternion":
self.inputs[2].name = "Quaternion XYZ"
self.inputs[3].name = "Quaternion W"
elif self.property0 == "Euler Angles":
self.inputs[2].name = "Euler Angles"
self.inputs[3].name = "[unused for Euler input]"
elif self.property0.startswith("Angle Axies"):
self.inputs[2].name = "Axis"
self.inputs[3].name = "Angle"
else:
raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_rotate_object.py')
def draw_buttons(self, context, layout):
# this block is here to ensure backwards compatibility and warn the user.
# delete it (only keep the "else" part) when the 'old version' of the node will be considered removed.
# (note: please also update the corresponding haxe file when doing so)
if len(self.inputs) < 4:
row = layout.row(align=True)
row.label(text="Node has been updated with armory 2020.09. Please consider deleting and recreating it.")
else:
layout.prop(self, 'property0')
layout.prop(self, 'property0_proxy')
# this property swaperoo is kinda janky-looking, but listen out:
# - when you reload an old file, the properties of loaded nodes can be mangled if the node class drops the property or the specific value within the property.
# -> to fix this, 'property0' needs to contain the old values so that node replacement can be done decently.
# - "but", I hear you ask, "why not make property0 a simple blender property, and create a property0v2 HaxeProperty to be bound to the haxe-time property0?"
# -> well, at the time of writing, a HaxeProperty's prop_name is only used for livepatching, not at initial setup, so a freshly-compiled game would get completely borked properties.
# solution: have a property0 HaxeProperty contain every possible value, and have a property0_proxy Property be in the UI.
# NOTE FOR FUTURE MAINTAINERS: the value of the proxy property does **not** matter, only the value of property0 does. When eventually editing this class, you can safely drop the values in the proxy property, and *only* the proxy property.
def on_proxyproperty_update(self, context=None):
self.property0 = self.property0_proxy
property0_proxy: EnumProperty(
items = [('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'),
('Global', 'Global/Parent F.O.R.',
'Frame of reference oriented with the object\'s parent or the world')],
name='', default='Local',
update = on_proxyproperty_update
)
property0: HaxeEnumProperty(
'property0',
items=[('Euler Angles', 'Euler Angles', 'Euler Angles'),
('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'),
('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='Euler Angles',
update=on_property_update)
items=[('Euler Angles', 'NODE REPLACEMENT ONLY', ''),
('Angle Axies (Radians)', 'NODE REPLACEMENT ONLY', ''),
('Angle Axies (Degrees)', 'NODE REPLACEMENT ONLY', ''),
('Quaternion', 'NODE REPLACEMENT ONLY', ''),
('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'),
('Global', 'Global/Parent F.O.R.',
'Frame of reference oriented with the object\'s parent or the world')
],
name='', default='Local')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNRotateObjectNode')
inputnode = self.id_data.nodes.new('LNRotationNode')
self.id_data.links.new(inputnode.outputs[0], newself.inputs[2])
newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw
inputnode.inputs[0].default_value = self.inputs[2].default_value
inputnode.inputs[1].default_value = self.inputs[3].default_value
if len(self.inputs[0].links) >0:
self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0])
if len(self.inputs[1].links) >0:
self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1])
if len(self.inputs[2].links) >0:
self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0])
if len(self.inputs[3].links) >0:
self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1])
# first, convert the default value
if self.property0 == 'Quaternion':
inputnode.property0 = 'Quaternion'
elif self.property0 == 'Euler Angles':
inputnode.property0 = 'EulerAngles'
inputnode.property1 = 'Rad'
inputnode.property2 = 'XZY' # legacy order
else: # starts with "Angle Axies"
inputnode.property0 = 'AxisAngle'
if 'Degrees' in self.property0:
inputnode.property1 = 'Deg'
else:
inputnode.property1 = 'Rad'
quat = Rotation.convert_to_quaternion(
self.inputs[2].default_value,
self.inputs[3].default_value,
inputnode.property0,
inputnode.property1,
inputnode.property2
)
newself.inputs[2].default_value_raw = quat
return [newself, inputnode]

View file

@ -0,0 +1,60 @@
from arm.logicnode.arm_nodes import *
class SeparateRotationNode(ArmLogicTreeNode):
"""Decompose a rotation into one of its mathematical representations"""
bl_idname = 'LNSeparateRotationNode'
bl_label = 'Separate Rotation'
arm_section = 'rotation'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmRotationSocket', 'Angle')
self.add_output('ArmVectorSocket', 'Euler Angles / Vector XYZ')
self.add_output('ArmFloatSocket', 'Angle / W')
def on_property_update(self, context):
"""called by the EnumProperty, used to update the node socket labels"""
if self.property0 == 'Quaternion':
self.outputs[0].name = "Quaternion XYZ"
self.outputs[1].name = "Quaternion W"
elif self.property0 == 'EulerAngles':
self.outputs[0].name = "Euler Angles"
self.outputs[1].name = "[unused for Euler output]"
elif self.property0 == 'AxisAngle':
self.outputs[0].name = "Axis"
self.outputs[1].name = "Angle"
else:
raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_separate_rotation.py')
def draw_buttons(self, context, layout):
coll = layout.column(align=True)
coll.prop(self, 'property0')
if self.property0 in ('EulerAngles','AxisAngle'):
coll.prop(self, 'property1')
if self.property0=='EulerAngles':
coll.prop(self, 'property2')
property0: HaxeEnumProperty(
'property0',
items = [('EulerAngles', 'Euler Angles', 'Euler Angles'),
('AxisAngle', 'Axis/Angle', 'Axis/Angle'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='EulerAngles',
update=on_property_update)
property1: HaxeEnumProperty(
'property1',
items=[('Deg', 'Degrees', 'Degrees'),
('Rad', 'Radians', 'Radians')],
name='', default='Rad')
property2: HaxeEnumProperty(
'property2',
items=[('XYZ','XYZ','XYZ'),
('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'),
('YXZ','YXZ','YXZ'),
('YZX','YZX','YZX'),
('ZXY','ZXY','ZXY'),
('ZYX','ZYX','ZYX')],
name='', default='XYZ')

View file

@ -4,11 +4,42 @@ class SeparateTransformNode(ArmLogicTreeNode):
"""Separates the transform of the given object."""
bl_idname = 'LNSeparateTransformNode'
bl_label = 'Separate Transform'
arm_version = 1
arm_version = 2
def arm_init(self, context):
self.add_input('ArmDynamicSocket', 'Transform')
self.add_output('ArmVectorSocket', 'Location')
self.add_output('ArmVectorSocket', 'Rotation')
self.add_output('ArmRotationSocket', 'Rotation')
self.add_output('ArmVectorSocket', 'Scale')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNSeparateTransformNode')
for link in self.outputs[0].links:
self.id_data.links.new(newself.outputs[0], link.to_socket)
for link in self.outputs[2].links:
self.id_data.links.new(newself.outputs[2], link.to_socket)
for link in self.inputs[0].links:
self.id_data.links.new(link.from_socket, newself.inputs[0])
ret = [newself]
rot_links = self.outputs[1].links
if len(rot_links) >0:
converter = self.id_data.nodes.new('LNSeparateRotationNode')
ret.append(converter)
self.id_data.links.new(newself.outputs[1], converter.inputs[0])
converter.property0 = 'EulerAngles'
converter.property1 = 'Rad'
converter.property2 = 'XZY'
for link in rot_links:
self.id_data.links.new(converter.outputs[0], link.to_socket)
return ret

View file

@ -1,42 +1,76 @@
from arm.logicnode.arm_nodes import *
from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation
class SetRotationNode(ArmLogicTreeNode):
"""Sets the rotation of the given object."""
bl_idname = 'LNSetRotationNode'
bl_label = 'Set Object Rotation'
arm_section = 'rotation'
arm_version = 1
arm_version = 2
def arm_init(self, context):
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('ArmVectorSocket', 'Euler Angles / Vector XYZ')
self.add_input('ArmFloatSocket', 'Angle / W')
self.add_input('ArmRotationSocket', 'Rotation')
self.add_output('ArmNodeSocketAction', 'Out')
def on_property_update(self, context):
"""called by the EnumProperty, used to update the node socket labels"""
if self.property0 == "Quaternion":
self.inputs[2].name = "Quaternion XYZ"
self.inputs[3].name = "Quaternion W"
elif self.property0 == "Euler Angles":
self.inputs[2].name = "Euler Angles"
self.inputs[3].name = "[unused for Euler input]"
elif self.property0.startswith("Angle Axies"):
self.inputs[2].name = "Axis"
self.inputs[3].name = "Angle"
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNRotateObjectNode')
inputnode = self.id_data.nodes.new('LNRotationNode')
self.id_data.links.new(inputnode.outputs[0], newself.inputs[2])
newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw
inputnode.inputs[0].default_value = self.inputs[2].default_value
inputnode.inputs[1].default_value = self.inputs[3].default_value
if len(self.inputs[0].links) >0:
self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0])
if len(self.inputs[1].links) >0:
self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1])
if len(self.inputs[2].links) >0:
self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0])
if len(self.inputs[3].links) >0:
self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1])
# first, convert the default value
if self.property0 == 'Quaternion':
inputnode.property0 = 'Quaternion'
elif self.property0 == 'Euler Angles':
inputnode.property0 = 'EulerAngles'
inputnode.property1 = 'Rad'
inputnode.property2 = 'XZY' # legacy order
elif self.property0.startswith("Angle Axies "):
inputnode.property0 = 'AxisAngle'
if 'Degrees' in self.property0:
inputnode.property1 = 'Deg'
else:
inputnode.property1 = 'Rad'
else:
raise ValueError('No nodesocket labels for current input mode: check self-consistancy of action_set_rotation.py')
raise ValueError('nonsensical value {:s} for property0 in SetObjectRotationNode/V1. please report this to the devs.'.format(self.property0))
quat = Rotation.convert_to_quaternion(
self.inputs[2].default_value,
self.inputs[3].default_value,
inputnode.property0,
inputnode.property1,
inputnode.property2
)
newself.inputs[2].default_value_raw = quat
return [newself, inputnode]
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
property0: HaxeEnumProperty(
'property0',
items = [('Euler Angles', 'Euler Angles', 'Euler Angles'),
('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'),
('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='Euler Angles',
update=on_property_update)
# note: this is unused, but kept here so that the 'property0' field can be read during node replacement
property0: EnumProperty(
items = [('Euler Angles', '',''),
('Angle Axies (Radians)', '', ''),
('Angle Axies (Degrees)', '', ''),
('Quaternion', '', '')],
name='', default='Euler Angles')

View file

@ -1,14 +0,0 @@
from arm.logicnode.arm_nodes import *
class TransformNode(ArmLogicTreeNode):
"""Stores the location, rotation and scale values as a transform."""
bl_idname = 'LNTransformNode'
bl_label = 'Transform'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Location')
self.add_input('ArmVectorSocket', 'Rotation')
self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0])
self.add_output('ArmDynamicSocket', 'Transform')

View file

@ -9,7 +9,7 @@ class VectorToObjectOrientationNode(ArmLogicTreeNode):
"""
bl_idname = 'LNVectorToObjectOrientationNode'
bl_label = 'Vector to Object Orientation'
arm_section = 'location'
arm_section = 'rotation'
arm_version = 1
def arm_init(self, context):

View file

@ -1,18 +0,0 @@
from arm.logicnode.arm_nodes import *
class QuaternionNode(ArmLogicTreeNode):
"""TO DO."""
bl_idname = 'LNQuaternionNode'
bl_label = 'Quaternion'
arm_section = 'quaternions'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmFloatSocket', 'X')
self.add_input('ArmFloatSocket', 'Y')
self.add_input('ArmFloatSocket', 'Z')
self.add_input('ArmFloatSocket', 'W', default_value=1.0)
self.add_output('ArmVectorSocket', 'Quaternion')
self.add_output('ArmVectorSocket', 'XYZ')
self.add_output('ArmFloatSocket', 'W')

View file

@ -0,0 +1,61 @@
from arm.logicnode.arm_nodes import *
class RotationNode(ArmLogicTreeNode):
"""A rotation, created from one of its possible mathematical representations"""
bl_idname = 'LNRotationNode'
bl_label = 'Rotation'
bl_description = 'Create a Rotation object, describing the difference between two orientations (internally represented as a quaternion for efficiency)'
#arm_section = 'rotation'
arm_version = 1
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Euler Angles / Vector XYZ')
self.add_input('ArmFloatSocket', 'Angle / W')
self.add_output('ArmRotationSocket', 'Out', is_var=True)
def on_property_update(self, context):
"""called by the EnumProperty, used to update the node socket labels"""
if self.property0 == "Quaternion":
self.inputs[0].name = "Quaternion XYZ"
self.inputs[1].name = "Quaternion W"
elif self.property0 == "EulerAngles":
self.inputs[0].name = "Euler Angles"
self.inputs[1].name = "[unused for Euler input]"
elif self.property0 == "AxisAngle":
self.inputs[0].name = "Axis"
self.inputs[1].name = "Angle"
else:
raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_rotation.py')
def draw_buttons(self, context, layout):
coll = layout.column(align=True)
coll.prop(self, 'property0')
if self.property0 in ('EulerAngles','AxisAngle'):
coll.prop(self, 'property1')
if self.property0=='EulerAngles':
coll.prop(self, 'property2')
property0: HaxeEnumProperty(
'property0',
items = [('EulerAngles', 'Euler Angles', 'Euler Angles'),
('AxisAngle', 'Axis/Angle', 'Axis/Angle'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='EulerAngles',
update=on_property_update)
property1: HaxeEnumProperty(
'property1',
items=[('Deg', 'Degrees', 'Degrees'),
('Rad', 'Radians', 'Radians')],
name='', default='Rad')
property2: HaxeEnumProperty(
'property2',
items=[('XYZ','XYZ','XYZ'),
('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'),
('YXZ','YXZ','YXZ'),
('YZX','YZX','YZX'),
('ZXY','ZXY','ZXY'),
('ZYX','ZYX','ZYX')],
name='', default='XYZ'
)

View file

@ -0,0 +1,44 @@
from arm.logicnode.arm_nodes import *
class TransformNode(ArmLogicTreeNode):
"""Stores the location, rotation and scale values as a transform."""
bl_idname = 'LNTransformNode'
bl_label = 'Transform'
arm_version = 2
def arm_init(self, context):
self.add_input('ArmVectorSocket', 'Location')
self.add_input('ArmRotationSocket', 'Rotation')
self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0])
self.add_output('ArmDynamicSocket', 'Transform', is_var=True)
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.arm_version not in (0, 1):
raise LookupError()
# transition from version 1 to version 2: make rotations their own sockets
# this transition is a mess, I know.
newself = self.id_data.nodes.new('LNTransformNode')
ret = [newself]
for link in self.inputs[0].links:
self.id_data.links.new(link.from_socket, newself.inputs[0])
for link in self.inputs[2].links:
self.id_data.links.new(link.from_socket, newself.inputs[2])
for link in self.outputs[0].links:
self.id_data.links.new(newself.outputs[0], link.to_socket)
links_rot = self.inputs[1].links
if len(links_rot) > 0:
converter = self.id_data.nodes.new('LNRotationNode')
self.id_data.links.new(converter.outputs[0], newself.inputs[1])
converter.property0 = 'EulerAngles'
converter.property1 = 'Rad'
converter.property2 = 'XZY'
ret.append(converter)
for link in links_rot:
self.id_data.links.new(link.from_socket, converter.inputs[0])
return ret

View file

@ -308,6 +308,8 @@ def build_default_node(inp: bpy.types.NodeSocket):
if inp_type == 'VECTOR':
return f'new armory.logicnode.VectorNode(this, {default_value})'
elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion.
return f'new armory.logicnode.RotationNode(this, {default_value})'
elif inp_type in ('RGB', 'RGBA'):
return f'new armory.logicnode.ColorNode(this, {default_value})'
elif inp_type == 'VALUE':