diff --git a/extensions/AlephOmega/SDFs.js b/extensions/AlephOmega/SDFs.js new file mode 100644 index 0000000000..81e540f409 --- /dev/null +++ b/extensions/AlephOmega/SDFs.js @@ -0,0 +1,591 @@ +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) { + throw new Error("This extension must run unsandboxed"); + } + + const max = Math.max; + const min = Math.min; + + class SDF { + getInfo() { + return { + color1: "#ad0090", + color2: "#6e145f", + color3: "#3b0932", + id: "AlephOmegaSDFS", + name: "SDFs", + blocks: [ + { + opcode: "Sphere", + blockType: Scratch.BlockType.REPORTER, + text: "Get distance from [P] to sphere at [A] with radius [R]", + arguments: { + P: { + type: Scratch.ArgumentType.STRING, + }, + A: { + type: Scratch.ArgumentType.STRING, + }, + R: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Box", + blockType: Scratch.BlockType.REPORTER, + text: "Get distance from [P] to box at [A] with size [S]", + arguments: { + P: { + type: Scratch.ArgumentType.STRING, + }, + A: { + type: Scratch.ArgumentType.STRING, + }, + S: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Box2", + blockType: Scratch.BlockType.REPORTER, + text: "Get distance from [P] to rounded box at [A] with size [S] with bevel [R]", + arguments: { + P: { + type: Scratch.ArgumentType.STRING, + }, + A: { + type: Scratch.ArgumentType.STRING, + }, + S: { + type: Scratch.ArgumentType.STRING, + }, + R: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Capsule", + blockType: Scratch.BlockType.REPORTER, + text: "Get distance from [P] to capsule from [A] to [B] with Radius [R]", + arguments: { + P: { + type: Scratch.ArgumentType.STRING, + }, + A: { + type: Scratch.ArgumentType.STRING, + }, + B: { + type: Scratch.ArgumentType.STRING, + }, + R: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Plane", + blockType: Scratch.BlockType.REPORTER, + text: "Get distance from [P] to plane with normal [A] and height (Perpendicular) [B]", + arguments: { + P: { + type: Scratch.ArgumentType.STRING, + }, + A: { + type: Scratch.ArgumentType.STRING, + }, + B: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Union", + blockType: Scratch.BlockType.REPORTER, + text: "[A] union [B] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Intersection", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Intersection [B] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Subtraction", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Subtraction [B] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Lerp", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Interpolate [B] by [t] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Champfer", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Champfer [B] by [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "ChampferInt", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Champfer intersect [B] by [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "ChampferSub", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Champfer Subtract [B] by [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Hollow", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Hollowed out with mid radius of [B] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Groove", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Groove [B] by [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "round_merge", + blockType: Scratch.BlockType.REPORTER, + text: "[A] round merge [B] with radius [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "round_intersect", + blockType: Scratch.BlockType.REPORTER, + text: "[A] round intersect [B] with radius [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "round_subtract", + blockType: Scratch.BlockType.REPORTER, + text: "[A] round subtract [B] with radius [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Smoothunion", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Smootly merge [B] with a coefficient of [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Smoothsub", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Smootly subtract [B] with a coefficient of [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Smoothint", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Smootly intersect [B] with a coefficient of [K] ", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + K: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Scale", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Scale by [B]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Rounded", + blockType: Scratch.BlockType.REPORTER, + text: "[A] round by radius of [B]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Onioning", + blockType: Scratch.BlockType.REPORTER, + text: "[A] onion by [B]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Edge", + blockType: Scratch.BlockType.REPORTER, + text: "[A] Get edge with thickness [B]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + ], + }; + } + + vecSub(v1, v2) { + return { x: v1.x - v2.x, y: v1.y - v2.y, z: v1.z - v2.z }; + } + + vecAdd(v1, v2) { + return { x: v1.x + v2.x, y: v1.y + v2.y, z: v1.z + v2.z }; + } + + vecMult(v, s) { + return { x: v.x * s, y: v.y * s, z: v.z * s }; + } + + dot(v1, v2) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + } + + vecDist(v1, v2) { + var dx = v1.x - v2.x; + var dy = v1.y - v2.y; + var dz = v1.z - v2.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + max(a, b) { + return Math.max(a, b); + } + + min(a, b) { + return Math.min(a, b); + } + + clamp(a, b, c) { + return max(min(a, b), c); + } + + Edge({ A, B }) { + return Math.abs(A + B) - B; + } + + Onioning({ A, B }) { + return Math.abs(A) - B; + } + + Round({ A, B }) { + return A - B; + } + + Smoothint({ A, B, K }) { + return A * (1 - Math.exp(-B / K)); + } + + Smoothsub({ A, B, K }) { + return A * Math.exp(-B / K); + } + + Smoothunion({ A, B, K }) { + return A + B - ((A * B) / (A + B)) * (1 - Math.exp(-(A + B) / K)); + } + + Scale({ A, B }) { + A = JSON.parse(A); + B = JSON.parse(B); + A.x = A.x * B.x; + A.y = A.y * B.y; + A.z = A.z * B.z; + return JSON.stringify(A); + } + Plane({ P, A, B }) { + var dot = A.x * P.x + A.y * P.y + A.z * P.z; + return dot - B; + } + + round_subtract({ A, B, K }) { + return this.round_intersect({ A: A, B: 0 - B, K: K }); + } + + round_intersect({ A, B, K }) { + var ISX = min(A + K, 0); + var ISY = min(B + K, 0); + var outdist = Math.sqrt(ISX * ISX + ISY * ISY); + var intersect = max(A, B); + var indist = min(intersect, -K); + return indist + outdist; + } + + round_merge({ A, B, K }) { + var ISX = max(A - K, 0); + var ISY = max(B - K, 0); + var indist = 0 - Math.sqrt(ISX * ISX + ISY * ISY); + var union = min(A, B); + var outdist = max(union, K); + return indist + outdist; + } + + Groove({ A, B, K }) { + return max(A, 0 - (Math.abs(B) - K)); + } + + Hollow({ A, B }) { + return Math.abs(A) - B; + } + + ChampferSub({ A, B, K }) { + var simpleMerge = max(A, 0 - B); + var champfer = (A + (0 - B)) * Math.sqrt(0.5); + champfer -= K; + return max(simpleMerge, champfer); + } + + ChampferInt({ A, B, K }) { + var simpleMerge = max(A, B); + var champfer = (A + B) * Math.sqrt(0.5); + champfer -= K; + return max(simpleMerge, champfer); + } + + Champfer({ A, B, K }) { + var simpleMerge = min(A, B); + var champfer = (A + B) * Math.sqrt(0.5); + champfer -= K; + return min(simpleMerge, champfer); + } + + Lerp({ A, B, T }) { + return A * T + B * (1 - T); + } + + Intersection({ A, B }) { + return max(A, B); + } + + Subtraction({ A, B }) { + return min(A, 0 - B); + } + + Union({ A, B }) { + return min(A, B); + } + + Capsule({ P, A, B, R }) { + P = JSON.parse(P); + A = JSON.parse(A); + B = JSON.parse(B); + var AP = this.vecSub(P, A); + var AB = this.vecSub(B, A); + var t = Math.max(0, Math.min(1, this.dot(AP, AB) / this.dot(AB, AB))); + var closestPoint = this.vecAdd(A, this.vecMult(AB, t)); + var distToClosest = this.vecDist(P, closestPoint); + if (t === 0 || t === 1) { + return distToClosest - R; + } else { + var distToAxis = this.vecDist(P, A) - R; + return Math.sqrt( + distToClosest * distToClosest + distToAxis * distToAxis + ); + } + } + + Sphere({ P, A, R }) { + P = JSON.parse(P); + A = JSON.parse(A); + var dx = P.x - A.x; + var dy = P.y - A.y; + var dz = P.z - A.z; + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + return dist - R; + } + + Box({ P, A, S }) { + P = JSON.parse(P); + A = JSON.parse(A); + var dx = P.x - A.x; + var dy = P.y - A.y; + var dz = P.z - A.z; + dx = Math.abs(dx) - S.x; + dy = Math.abs(dy) - S.y; + dz = Math.abs(dz) - S.z; + dx = max(dx, 0); + dy = max(dy, 0); + dz = max(dz, 0); + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + return dist + min(max(dx, max(dy, dz)), 0); + } + + Box2({ P, A, S, R }) { + P = JSON.parse(P); + A = JSON.parse(A); + var dx = P.x - A.x; + var dy = P.y - A.y; + var dz = P.z - A.z; + dx = Math.abs(dx) - S.x; + dy = Math.abs(dy) - S.y; + dz = Math.abs(dz) - S.z; + dx = max(dx, 0); + dy = max(dy, 0); + dz = max(dz, 0); + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + return dist + min(max(dx, max(dy, dz)), 0) - R; + } + } + + Scratch.extensions.register(new SDF()); +})(Scratch); diff --git a/extensions/AlephOmega/VectorUtils.js b/extensions/AlephOmega/VectorUtils.js new file mode 100644 index 0000000000..04a39f2bd5 --- /dev/null +++ b/extensions/AlephOmega/VectorUtils.js @@ -0,0 +1,653 @@ +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) { + throw new Error("This Vector extension must run unsandboxed"); + } + const menuIconURI = + "data:image/png;base64,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"; + const blockIconURI = + "data:image/png;base64,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"; + class VectorExtension { + getInfo() { + return { + id: "AlephOmegaVector", + name: "VectorUtils", + color1: "#4286f4", + color2: "#63a8f5", + color3: "#a3c9f7", + menuIconURI: menuIconURI, + blockIconURI: blockIconURI, + blocks: [ + { + opcode: "getVector", + blockType: Scratch.BlockType.REPORTER, + text: "create vector with x: [x] y: [y] z: [z]", + arguments: { + x: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + z: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "addVectors", + blockType: Scratch.BlockType.REPORTER, + text: "add vector [vector1] and [vector2]", + arguments: { + vector1: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + vector2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "subtractVectors", + blockType: Scratch.BlockType.REPORTER, + text: "subtract vector [vector1] from [vector2]", + arguments: { + vector1: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + vector2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "calculateMagnitude", + blockType: Scratch.BlockType.REPORTER, + text: "magnitude of vector [vector]", + arguments: { + vector: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "multiplyVector", + blockType: Scratch.BlockType.REPORTER, + text: "multiply vector [vector] by scalar [scalar]", + arguments: { + vector: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + scalar: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "dotProduct", + blockType: Scratch.BlockType.REPORTER, + text: "dot product of [vector1] and [vector2]", + arguments: { + vector1: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + vector2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "crossProduct", + blockType: Scratch.BlockType.REPORTER, + text: "cross product of [vector1] and [vector2]", + arguments: { + vector1: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + vector2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "Rotate", + blockType: Scratch.BlockType.REPORTER, + text: "Rotate [VEC] by [Vector]", + arguments: { + VEC: { + type: Scratch.ArgumentType.STRING, + }, + Vector: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Normalize", + blockType: Scratch.BlockType.REPORTER, + text: "Normalize [VECTOR]", + arguments: { + VECTOR: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Angle", + blockType: Scratch.BlockType.REPORTER, + text: "Angle between [V1] and [V2]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + V2: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "ProjectionOn", + blockType: Scratch.BlockType.REPORTER, + text: "Project [V1] onto [V2]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + V2: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "ProjectionFrom", + blockType: Scratch.BlockType.REPORTER, + text: "Project [V1] From [V2]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + V2: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "get", + blockType: Scratch.BlockType.REPORTER, + text: "Get [VAL] from [VECTOR]", + arguments: { + VAL: { + type: Scratch.ArgumentType.STRING, + menu: "VAL", + }, + VECTOR: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Reflect", + blockType: Scratch.BlockType.REPORTER, + text: "Reflect vector [Vector] onto plane with [Normal] as its normal", + arguments: { + Vector: { + type: Scratch.ArgumentType.STRING, + }, + type: Scratch.ArgumentType.STRING, + }, + }, + { + opcode: "Setmag", + blockType: Scratch.BlockType.REPORTER, + text: "Set magnitude of [VEC] to [MAG]", + arguments: { + VEC: { + type: Scratch.ArgumentType.STRING, + }, + MAG: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "lerp", + blockType: Scratch.BlockType.REPORTER, + text: "Interpolate [V1] towards [V2] with an alpha of [T]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + V2: { + type: Scratch.ArgumentType.STRING, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Mat", + blockType: Scratch.BlockType.REPORTER, + text: "Multiply [VEC] by Matrix X [XV] Y [YV] Z [ZV]", + arguments: { + VEC: { + type: Scratch.ArgumentType.STRING, + }, + XV: { + type: Scratch.ArgumentType.STRING, + }, + YV: { + type: Scratch.ArgumentType.STRING, + }, + ZV: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Distance", + blockType: Scratch.BlockType.REPORTER, + text: "distance between [V1] and [V2]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + V2: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "Set", + blockType: Scratch.BlockType.REPORTER, + text: "Set [SET] of [OF] to [TO]", + arguments: { + SET: { + type: Scratch.ArgumentType.STRING, + menu: "VAL", + }, + OF: { + type: Scratch.ArgumentType.STRING, + }, + TO: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "Abs", + blockType: Scratch.BlockType.REPORTER, + text: "Absolute value of all axis in [V1]", + arguments: { + V1: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + ], + menus: { + VAL: { + acceptReporters: true, + items: ["X", "Y", "Z"], + }, + }, + }; + } + + isValidJSON(jsonString) { + try { + const parsedJSON = JSON.parse(jsonString); + return ( + typeof parsedJSON === "object" && + !Array.isArray(parsedJSON) && + this.areAllValuesNumbers(parsedJSON) + ); + } catch (error) { + return false; + } + } + + areAllValuesNumbers(obj) { + const requiredKeys = ["x", "y", "z"]; + for (const key of requiredKeys) { + if (!(key in obj) || typeof obj[key] !== "number") { + return false; + } + } + return true; + } + + Abs(args) { + if (!this.isValidJSON(args.V1)) { + return "Make sure all inputs are valid"; + } + var V1 = JSON.parse(args.V1); + V1.X = Math.abs(V1.X); + V1.Y = Math.abs(V1.Y); + V1.Z = Math.abs(V1.Z); + return JSON.stringify(V1); + } + + Set(args) { + if ( + !( + this.isValidJSON(args.SET) && + (args.OF == "X" || args.OF == "Y" || args.OF == "Z") && + typeof args.OF == "number" + ) + ) { + return "Make sure all inputs are valid"; + } + var { SET, OF, TO } = args; + OF = JSON.parse(OF); + if (SET == "X") { + OF.X = TO; + } + if (SET == "Y") { + OF.Y = TO; + } + if (SET == "Z") { + OF.Z = TO; + } + return JSON.stringify(OF); + } + + Distance(args) { + if (!(this.isValidJSON(args.V1) && this.isValidJSON(args.V2))) { + return "Make sure all inputs are valid"; + } + var { V1, V2 } = args; + V1 = JSON.parse(V1); + V2 = JSON.parse(V2); + + var dx = V2.x - V1.x; + var dy = V2.y - V1.y; + var dz = V2.z - V1.z; + + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + Mat(args) { + var { VEC, XV, YV, ZV } = args; + if ( + !( + this.isValidJSON(VEC) && + this.isValidJSON(XV) && + this.isValidJSON(YV) && + this.isValidJSON(ZV) + ) + ) { + return "Make sure all inputs are valid"; + } + var x = VEC.x * XV.x + VEC.y * XV.y + VEC.y * XV.y; + var y = VEC.x * YV.x + VEC.y * YV.y + VEC.y * YV.y; + var z = VEC.z * ZV.x + VEC.y * ZV.y + VEC.y * ZV.y; + return JSON.stringify({ x: x, y: y, z: z }); + } + + lerp(args) { + var { V1, V2, T } = args; + if ( + !(this.isValidJSON(V1) && this.isValidJSON(V2) && typeof t == "number") + ) { + return "Make sure all inputs are valid"; + } + var scaled = V1; + scaled.x *= T; + scaled.y *= T; + scaled.z *= T; + var scaled2 = V2; + scaled2 *= 1 - T; + scaled2 *= 1 - T; + scaled2 *= 1 - T; + var end = V1; + end.x += scaled2.x; + end.y += scaled2.y; + end.z += scaled2.z; + return JSON.stringify(end); + } + + Setmag(args) { + var { VEC, MAG } = args; + if (!(this.isValidJSON(VEC) && typeof MAG == "number")) { + return "Make sure all inputs are valid"; + } + VEC = JSON.parse(VEC); + var Mag = Math.sqrt(VEC.x * VEC.x + VEC.y * VEC.y + VEC.z * VEC.z); + var scalingfactor = MAG / Mag; + VEC.x *= scalingfactor; + VEC.y *= scalingfactor; + VEC.z *= scalingfactor; + return JSON.stringify(VEC); + } + Reflect(args) { + var { Vector, Normal } = args; + if (!(this.isValidJSON(Vector) && this.isValidJSON(Normal))) { + return "Make sure all inputs are valid"; + } + Vector = JSON.parse(Vector); + Normal = JSON.parse(Normal); + var dot = Vector.x * Normal.x + Vector.y * Normal.y + Vector.z * Normal.z; + var scaled = Normal; + scaled.x *= dot * 2; + scaled.y *= dot * 2; + scaled.z *= dot * 2; + + var reflected = Vector; + reflected.x -= scaled.x; + reflected.y -= scaled.y; + reflected.z -= scaled.z; + + return JSON.stringify(reflected); + } + + get(args) { + var { VAL, VECTOR } = args; + if (!this.isValidJSON(VECTOR)) { + return "Make sure all inputs are valid"; + } + VECTOR = JSON.parse(VECTOR); + if (VAL == "X") { + return VECTOR.x; + } + if (VAL == "Y") { + return VECTOR.y; + } + if (VAL == "Z") { + return VECTOR.z; + } + } + + ProjectionFrom(args) { + if (!(this.isValidJSON(args.V1) && this.isValidJSON(args.V2))) { + return "Make sure all inputs are valid"; + } + var V1 = JSON.parse(args.V1); + var V2 = JSON.parse(args.V2); + + const dot = V2.x * V1.x + V2.y * V1.y + V2.z * V1.z; + var Pfrom = V1; + Pfrom.x *= dot; + Pfrom.y *= dot; + Pfrom.Z *= dot; + var from = V2; + from.x -= Pfrom.x; + from.y -= Pfrom.y; + from.z -= Pfrom.z; + return JSON.stringify(from); + } + + ProjectionOn(args) { + var { V1, V2 } = args; + if (!(this.isValidJSON(V1) && this.isValidJSON(V2))) { + return "Make sure all inputs are valid"; + } + V1 = JSON.parse(V1); + V2 = JSON.parse(V2); + const dot = V2.x * V1.x + V2.y * V1.y + V2.z * V1.z; + const Pfrom = V1; + Pfrom.x *= dot; + Pfrom.y *= dot; + Pfrom.Z *= dot; + return JSON.stringify(Pfrom); + } + + Angle(args) { + var { V1, V2 } = args; + if (!(this.isValidJSON(V1) && this.isValidJSON(V2))) { + return "Make sure all inputs are valid"; + } + V1 = JSON.parse(V1); + V2 = JSON.parse(V2); + const mag1 = Math.sqrt(V1.x * V1.x + V1.y * V1.y + V1.z * V1.z); + const mag2 = Math.sqrt(V2.x * V2.x + V2.y * V2.y + V2.z * V2.z); + const dot = V2.x * V1.x + V2.y * V1.y + V2.z * V1.z; + return Math.acos(dot / (mag1 * mag2)) * (180 / Math.PI); + } + + Normalize(args) { + if (!this.isValidJSON(args.VECTOR)) { + return "Make sure all inputs are valid"; + } + var vec = args.VECTOR; + vec = JSON.parse(vec); + const magnitude = Math.sqrt( + vec.x * vec.x + vec.y * vec.y + vec.z * vec.z + ); + vec.x /= magnitude; + vec.y /= magnitude; + vec.z /= magnitude; + return JSON.stringify(vec); + } + + Rotate(args) { + const { Vector, VEC } = args; + if (!(this.isValidJSON(Vector) && this.isValidJSON(VEC))) { + return "Make sure all inputs are valid"; + } + var vector = JSON.parse(VEC); + var angle = JSON.parse(Vector); + angle.x *= Math.PI / 180; + angle.y *= Math.PI / 180; + angle.z *= Math.PI / 180; + var rx = vector.x * Math.cos(angle.y) + vector.z * Math.sin(angle.y); + vector.z = vector.z * Math.cos(angle.y) - vector.x * Math.sin(angle.y); + vector.x = rx; + var ry = vector.y * Math.cos(angle.y) + vector.z * Math.sin(angle.y); + vector.z = vector.z * Math.cos(angle.y) - vector.y * Math.sin(angle.y); + vector.y = ry; + var rz = vector.x * Math.cos(angle.z) - vector.y * Math.sin(angle.z); + vector.y = vector.y * Math.cos(angle.z) + vector.x * Math.sin(angle.z); + vector.x = rz; + return JSON.stringify(vector); + } + + getVector(args) { + const { x, y, z } = args; + if ( + !(typeof x == "number" && typeof y == "number" && typeof z == "number") + ) { + return "Make sure all inputs are valid"; + } + const vector = { x, y, z }; + return JSON.stringify(vector); + } + + addVectors(args) { + const { vector1, vector2 } = args; + if (!(this.isValidJSON(vector1) && this.isValidJSON(vector2))) { + return "Make sure all inputs are valid"; + } + const vec1 = JSON.parse(vector1); + const vec2 = JSON.parse(vector2); + const result = { + x: Number(vec1.x) + Number(vec2.x), + y: Number(vec1.y) + Number(vec2.y), + z: Number(vec1.z) + Number(vec2.z), + }; + return JSON.stringify(result); + } + + subtractVectors(args) { + const { vector1, vector2 } = args; + if (!(this.isValidJSON(vector1) && this.isValidJSON(vector2))) { + return "Make sure all inputs are valid"; + } + const vec1 = JSON.parse(vector1); + const vec2 = JSON.parse(vector2); + const result = { + x: Number(vec2.x) - Number(vec1.x), + y: Number(vec2.y) - Number(vec1.y), + z: Number(vec2.z) - Number(vec1.z), + }; + return JSON.stringify(result); + } + + calculateMagnitude(args) { + const { vector } = args; + if (!this.isValidJSON(vector)) { + return "Make sure all inputs are valid"; + } + const vec = JSON.parse(vector); + const magnitude = Math.sqrt( + vec.x * vec.x + vec.y * vec.y + vec.z * vec.z + ); + return magnitude; + } + + multiplyVector(args) { + const { vector, scalar } = args; + if (!(this.isValidJSON(vector) && typeof scalar == "number")) { + return "Make sure all inputs are valid"; + } + const vec = JSON.parse(vector); + const result = { + x: vec.x * scalar, + y: vec.y * scalar, + z: vec.z * scalar, + }; + return JSON.stringify(result); + } + + dotProduct(args) { + const { vector1, vector2 } = args; + if (!(this.isValidJSON(vector1) && this.isValidJSON(vector2))) { + return "Make sure all inputs are valid"; + } + const vec1 = JSON.parse(vector1); + const vec2 = JSON.parse(vector2); + const result = vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z; + return result; + } + + crossProduct(args) { + const { vector1, vector2 } = args; + if (!(this.isValidJSON(vector1) && this.isValidJSON(vector2))) { + return "Make sure all inputs are valid"; + } + const vec1 = JSON.parse(vector1); + const vec2 = JSON.parse(vector2); + const result = { + x: vec1.y * vec2.z - vec1.z * vec2.y, + y: vec1.z * vec2.x - vec1.x * vec2.z, + z: vec1.x * vec2.y - vec1.y * vec2.x, + }; + return JSON.stringify(result); + } + } + Scratch.extensions.register(new VectorExtension()); +})(Scratch); diff --git a/images/AlephOmega/SDF.svg b/images/AlephOmega/SDF.svg new file mode 100644 index 0000000000..dbee4acb4e --- /dev/null +++ b/images/AlephOmega/SDF.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/images/AlephOmega/VectorUtils.svg b/images/AlephOmega/VectorUtils.svg new file mode 100644 index 0000000000..51c3845c6a --- /dev/null +++ b/images/AlephOmega/VectorUtils.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +