diff --git a/src/lib/extensions.js b/src/lib/extensions.js
index 09657d34..ee15a425 100644
--- a/src/lib/extensions.js
+++ b/src/lib/extensions.js
@@ -13,6 +13,14 @@ export default [
creator: "MikeDev101",
isGitHub: true,
},
+ {
+ name: "Vectors",
+ description: "Adds a set of blocks for vector operations in 2D and 3D space.",
+ code: "Cool_skratcher/vectors.js",
+ banner: "Cool_skratcher/vectors.svg",
+ creator: "cool_dev",
+ isGitHub: true,
+ },
{
name: "Pen+",
description: "Extended pen section! Adds blocks for drawing triangles using textures and tints, drawing images and editing their pixels, etc.",
diff --git a/static/extensions/Cool_skratcher/vectors.js b/static/extensions/Cool_skratcher/vectors.js
new file mode 100644
index 00000000..a6d4ab2c
--- /dev/null
+++ b/static/extensions/Cool_skratcher/vectors.js
@@ -0,0 +1,477 @@
+// Name: Vectors!
+// ID: cooldevvectors
+// Description: Adds a set of blocks for vector operations in 2D and 3D space.
+// By: cool_skratcher
+// License: MIT
+
+(function(Scratch) {
+ 'use strict';
+ const vm = Scratch.vm;
+ const mouse = vm.runtime.ioDevices.mouse;
+
+ let penPLoaded = false;
+ let penPModule = null;
+ const penPCheck = () => {
+ if (penPLoaded) {
+ return;
+ }
+ if (vm.runtime.ext_penP) {
+ penPLoaded = true;
+ penPModule = vm.runtime.ext_penP;
+
+ if (penPModule) {
+ penPModule.turnAdvancedSettingOff({
+ Setting: "wValueUnderFlow",
+ onOrOff: "on",
+ });
+ }
+
+ vm.runtime.extensionManager.refreshBlocks();
+ }
+ };
+ penPCheck();
+
+ class VecMath {
+ constructor() {
+ this.camera = { pos: { x: 0, y: 0, z: 0 }, rot: { yaw: 0, pitch: 0, roll: 0 }, fov: 100 };
+ Scratch.vm.runtime.on("EXTENSION_ADDED", () => {
+ penPCheck();
+ console.log(vm.runtime.ext_penP)
+ });
+ }
+
+ getInfo() {
+ return {
+ id: 'cooldevvectors',
+ name: 'Vectors',
+ color1: "#57a3e5",
+ color2: "#0063ba",
+ blocks: [
+ "---",
+ { text: 'Vector Operations', blockType: Scratch.BlockType.LABEL},
+ { opcode: 'vec2', blockType: Scratch.BlockType.REPORTER, text: 'vec2 [x] [y]', arguments: { x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } }},
+ { opcode: 'vec3', blockType: Scratch.BlockType.REPORTER, text: 'vec3 [x] [y] [z]', arguments: { x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, z: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }}},
+ { opcode: 'getComponent', blockType: Scratch.BlockType.REPORTER, text: '[component] of [v]', arguments: { component: { type: Scratch.ArgumentType.STRING, menu: 'components' }, v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'add', blockType: Scratch.BlockType.REPORTER, text: 'add [v1] [v2]', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'sub', blockType: Scratch.BlockType.REPORTER, text: 'sub [v1] [v2]', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'mul', blockType: Scratch.BlockType.REPORTER, text: 'mul [v] by [scalar]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, scalar: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }}},
+ { opcode: 'div', blockType: Scratch.BlockType.REPORTER, text: 'div [v] by [scalar]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, scalar: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }}},
+ { opcode: 'mag', blockType: Scratch.BlockType.REPORTER, text: 'mag [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'dot', blockType: Scratch.BlockType.REPORTER, text: 'dot [v1] [v2]', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'cross', blockType: Scratch.BlockType.REPORTER, text: 'cross [v1] [v2]', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'rotateVec3', blockType: Scratch.BlockType.REPORTER, text: 'rotate [v] by x: [rx] y: [ry] z: [rz]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, rx: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, ry: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, rz: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }}},
+ { opcode: 'rotateAroundOrigin', blockType: Scratch.BlockType.REPORTER, text: 'rotate [v] around [origin] by x: [rx] y: [ry] z: [rz]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, origin: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, rx: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, ry: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, rz: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }}},
+ "---",
+ { text: 'Culling', blockType: Scratch.BlockType.LABEL},
+ { opcode: 'isCulled', blockType: Scratch.BlockType.BOOLEAN, text: 'is [v] culled', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'cullAgainstPlane', blockType: Scratch.BlockType.BOOLEAN, text: 'is [v] culled against plane [plane]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, plane: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,1' }}},
+ { opcode: 'frustumCull', blockType: Scratch.BlockType.BOOLEAN, text: 'is [v] inside frustum with near: [near] far: [far]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, near: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1 }, far: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }}},
+ { opcode: 'backFaceCull', blockType: Scratch.BlockType.BOOLEAN, text: 'is face [v1] [v2] [v3] culled', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '1,0,0' }, v3: { type: Scratch.ArgumentType.STRING, defaultValue: '0,1,0' }}},
+ "---",
+ { text: 'Projection and Camera', blockType: Scratch.BlockType.LABEL},
+ { opcode: 'project', blockType: Scratch.BlockType.REPORTER, text: 'project [v] to screen', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'setCamPos', blockType: Scratch.BlockType.COMMAND, text: 'set camera pos to [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'changeCamPos', blockType: Scratch.BlockType.COMMAND, text: 'change camera pos by [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'changeCamComponent', blockType: Scratch.BlockType.COMMAND, text: 'change camera [component] by [val]', arguments: { component: { type: Scratch.ArgumentType.STRING, menu: 'components' }, val: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }}},
+ { opcode: 'getCamPos', blockType: Scratch.BlockType.REPORTER, text: 'camera pos' },
+
+ { opcode: 'setCamRot', blockType: Scratch.BlockType.COMMAND, text: 'set camera rotation to [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'changeCamRot', blockType: Scratch.BlockType.COMMAND, text: 'change camera rotation by [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0,0' }}},
+ { opcode: 'changeCamAngle', blockType: Scratch.BlockType.COMMAND, text: 'change camera [angle] by [val]', arguments: { angle: { type: Scratch.ArgumentType.STRING, menu: 'angles' }, val: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }}},
+ { opcode: 'getCamRot', blockType: Scratch.BlockType.REPORTER, text: 'camera rotation' },
+
+ { opcode: 'setFOV', blockType: Scratch.BlockType.COMMAND, text: 'set FOV to [val]', arguments: { val: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }}},
+ { opcode: 'changeFOV', blockType: Scratch.BlockType.COMMAND, text: 'change FOV by [val]', arguments: { val: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }}},
+ { opcode: 'getFOV', blockType: Scratch.BlockType.REPORTER, text: 'FOV' },
+ "---",
+ { text: 'Sprite Movement', blockType: Scratch.BlockType.LABEL },
+ { opcode: 'moveByVec2', blockType: Scratch.BlockType.COMMAND, text: 'change pos by vec2 [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0' }}},
+ { opcode: 'goToVec2', blockType: Scratch.BlockType.COMMAND, text: 'go to vec2 [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0' }}},
+ { opcode: 'pointTo', blockType: Scratch.BlockType.COMMAND, text: 'point to vec2 [v]', arguments: { v: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0' }}},
+ { opcode: 'getPosVec2', blockType: Scratch.BlockType.REPORTER, text: 'get position as vec2' },
+ { opcode: 'getSizeVec2', blockType: Scratch.BlockType.REPORTER, text: 'get size as vec2' },
+ { opcode: 'getMousePosVec2', blockType: Scratch.BlockType.REPORTER, text: 'mouse pos vec2' },
+ "---",
+ { hideFromPalette: !penPLoaded, text: 'Pen+', blockType: Scratch.BlockType.LABEL },
+ { hideFromPalette: !penPLoaded, opcode: 'drawTriangle', blockType: Scratch.BlockType.COMMAND, text: 'triangle between [v1] [v2] [v3]', arguments: { v1: { type: Scratch.ArgumentType.STRING, defaultValue: '0,0' }, v2: { type: Scratch.ArgumentType.STRING, defaultValue: '1,0' }, v3: { type: Scratch.ArgumentType.STRING, defaultValue: '0,1' }}},
+ ],
+ menus: {
+ components: { items: ['x', 'y', 'z'] },
+ angles: { items: ['yaw', 'pitch', 'roll'] },
+ }
+ };
+ }
+
+ // Vector creation
+ vec2(args) {
+ const x = Scratch.Cast.toNumber(args.x) || 0;
+ const y = Scratch.Cast.toNumber(args.y) || 0;
+ return `${x},${y}`;
+ }
+
+ vec3(args) {
+ const x = Scratch.Cast.toNumber(args.x) || 0;
+ const y = Scratch.Cast.toNumber(args.y) || 0;
+ const z = Scratch.Cast.toNumber(args.z) || 0;
+ return `${x},${y},${z}`;
+ }
+
+ // Vector component access
+ getComponent(args) {
+ const [x, y, z = 0] = args.v.split(',').map(Number);
+ return args.component === 'x' ? x : args.component === 'y' ? y : z;
+ }
+
+ // Basic vector math
+ add(args) {
+ return this._vectorOp(args.v1, args.v2, (a, b) => (a || 0) + (b || 0));
+ }
+
+ sub(args) {
+ return this._vectorOp(args.v1, args.v2, (a, b) => (a || 0) - (b || 0));
+ }
+
+ mul(args) {
+ const scalar = Scratch.Cast.toNumber(args.scalar) || 0;
+ return this._scalarOp(args.v, scalar, (a, b) => (a || 0) * b);
+ }
+
+ div(args) {
+ const scalar = Scratch.Cast.toNumber(args.scalar) || 1;
+ return this._scalarOp(args.v, scalar, (a, b) => b !== 0 ? (a || 0) / b : 0);
+ }
+
+ mag(args) {
+ const components = args.v.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ return Math.hypot(...components);
+ }
+
+ // Fixed dot product with validation
+ dot(args) {
+ const v1 = args.v1.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const v2 = args.v2.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const [x1 = 0, y1 = 0, z1 = 0] = v1;
+ const [x2 = 0, y2 = 0, z2 = 0] = v2;
+ return x1 * x2 + y1 * y2 + z1 * z2;
+ }
+
+ // Improved cross product
+ cross(args) {
+ const v1 = args.v1.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const v2 = args.v2.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const [x1 = 0, y1 = 0, z1 = 0] = v1;
+ const [x2 = 0, y2 = 0, z2 = 0] = v2;
+ return `${y1 * z2 - z1 * y2},${z1 * x2 - x1 * z2},${x1 * y2 - y1 * x2}`;
+ }
+
+ // Fixed rotation functions
+ rotateVec3(args) {
+ const [x = 0, y = 0, z = 0] = args.v.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const rx = this._toRadians(Scratch.Cast.toNumber(args.rx) || 0);
+ const ry = this._toRadians(Scratch.Cast.toNumber(args.ry) || 0);
+ const rz = this._toRadians(Scratch.Cast.toNumber(args.rz) || 0);
+ return this._rotate3D({ x, y, z }, rx, ry, rz);
+ }
+
+ // Camera Controls
+ setCamPos(args) { this._setVec(this.camera.pos, args.v); }
+ changeCamPos(args) { this._addVec(this.camera.pos, args.v); }
+ changeCamComponent(args) { this.camera.pos[args.component] += args.val; }
+ getCamPos() { return `${this.camera.pos.x},${this.camera.pos.y},${this.camera.pos.z}`; }
+
+ setCamRot(args) { this._setRot(this.camera.rot, args.v); }
+ changeCamRot(args) { this._addRot(this.camera.rot, args.v); }
+ changeCamAngle(args) { this.camera.rot[args.angle] += args.val; }
+ getCamRot() { return `${this.camera.rot.yaw},${this.camera.rot.pitch},${this.camera.rot.roll}`; }
+
+ setFOV(args) { this.camera.fov = args.val; }
+ changeFOV(args) { this.camera.fov += args.val; }
+ getFOV() { return this.camera.fov; }
+
+ // Projection
+ project(args) {
+ const [x = 0, y = 0, z = 0] = args.v.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const cam = this.camera;
+
+ // Apply transformations in correct order
+ const dx = x - cam.pos.x;
+ const dy = y - cam.pos.y;
+ const dz = z - cam.pos.z;
+
+ // Convert angles to radians for consistency
+ const yawRad = this._toRadians(cam.rot.yaw);
+ const pitchRad = this._toRadians(cam.rot.pitch);
+ const rollRad = this._toRadians(cam.rot.roll);
+
+ // Apply rotations in correct order: yaw -> pitch -> roll
+ // First yaw (around Y axis)
+ let cx = dx * Math.cos(yawRad) - dz * Math.sin(yawRad);
+ let cz = dx * Math.sin(yawRad) + dz * Math.cos(yawRad);
+ let cy = dy;
+
+ // Then pitch (around X axis)
+ const tempZ = cz * Math.cos(pitchRad) - cy * Math.sin(pitchRad);
+ cy = cz * Math.sin(pitchRad) + cy * Math.cos(pitchRad);
+ cz = tempZ;
+
+ // Finally roll (around Z axis)
+ const finalX = cx * Math.cos(rollRad) - cy * Math.sin(rollRad);
+ const finalY = cx * Math.sin(rollRad) + cy * Math.cos(rollRad);
+
+ // Prevent division by zero
+ if (Math.abs(cz) < 0.0001) {
+ return '0,0';
+ }
+
+ // Calculate screen coordinates with FOV
+ const screenX = (finalX / cz) * cam.fov;
+ const screenY = (finalY / cz) * cam.fov;
+
+ return `${screenX},${screenY}`;
+ }
+
+ rotateAroundOrigin(args) {
+ const [vx, vy, vz] = args.v.split(',').map(Number);
+ const [ox, oy, oz] = args.origin.split(',').map(Number);
+ const rx = this._toRadians(args.rx);
+ const ry = this._toRadians(args.ry);
+ const rz = this._toRadians(args.rz);
+
+ // Translate vector to origin
+ let x = vx - ox;
+ let y = vy - oy;
+ let z = vz - oz;
+
+ // Rotate around Z-axis
+ let tempX = x * Math.cos(rz) - y * Math.sin(rz);
+ let tempY = x * Math.sin(rz) + y * Math.cos(rz);
+ x = tempX; y = tempY;
+
+ // Rotate around Y-axis
+ tempX = x * Math.cos(ry) + z * Math.sin(ry);
+ let tempZ = -x * Math.sin(ry) + z * Math.cos(ry);
+ x = tempX; z = tempZ;
+
+ // Rotate around X-axis
+ tempY = y * Math.cos(rx) - z * Math.sin(rx);
+ tempZ = y * Math.sin(rx) + z * Math.cos(rx);
+ y = tempY; z = tempZ;
+
+ // Translate back to original position
+ x += ox;
+ y += oy;
+ z += oz;
+
+ return `${x},${y},${z}`;
+ }
+
+ isCulled(args) {
+ const [x, y, z] = args.v.split(',').map(Number);
+ const { pos, rot } = this.camera;
+
+ // Transform point relative to camera
+ const dx = x - pos.x;
+ const dy = y - pos.y;
+ const dz = z - pos.z;
+
+ // Simple culling: check if behind the camera (z <= 0 in camera space)
+ const camZ = dz * Math.cos(rot.yaw) + dx * Math.sin(rot.yaw);
+ return camZ <= 0;
+ }
+
+ cullAgainstPlane(args) {
+ const [x, y, z] = args.v.split(',').map(Number);
+ const [nx, ny, nz] = args.plane.split(',').map(Number);
+
+ // Plane culling: check if point is behind the plane (dot product <= 0)
+ const dot = x * nx + y * ny + z * nz;
+ return dot <= 0;
+ }
+
+ frustumCull(args) {
+ const [x = 0, y = 0, z = 0] = args.v.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const { pos, rot, fov } = this.camera;
+ const near = Math.max(0.1, Scratch.Cast.toNumber(args.near) || 0.1);
+ const far = Math.max(near + 0.1, Scratch.Cast.toNumber(args.far) || 100);
+
+ // Transform point to camera space
+ const dx = x - pos.x;
+ const dy = y - pos.y;
+ const dz = z - pos.z;
+
+ // Convert angles to radians
+ const yawRad = this._toRadians(rot.yaw);
+ const pitchRad = this._toRadians(rot.pitch);
+
+ // Apply rotations (yaw then pitch)
+ let cx = dx * Math.cos(yawRad) - dz * Math.sin(yawRad);
+ let cz = dx * Math.sin(yawRad) + dz * Math.cos(yawRad);
+ let cy = dy;
+
+ // Apply pitch
+ const temp_z = cz * Math.cos(pitchRad) - cy * Math.sin(pitchRad);
+ cy = cz * Math.sin(pitchRad) + cy * Math.cos(pitchRad);
+ cz = temp_z;
+
+ // Early exit if behind camera
+ if (cz <= 0) return true;
+
+ // Check against near/far planes
+ if (cz < near || cz > far) return true;
+
+ // Calculate FOV bounds with correct aspect ratio (4:3)
+ const hFovRad = this._toRadians(fov);
+ const vFovRad = 2 * Math.atan(Math.tan(hFovRad / 2) * (3/4));
+
+ // Check against frustum planes
+ const hHalf = Math.abs(Math.tan(hFovRad / 2) * cz);
+ const vHalf = Math.abs(Math.tan(vFovRad / 2) * cz);
+
+ return Math.abs(cx) > hHalf || Math.abs(cy) > vHalf;
+ }
+
+ backFaceCull(args) {
+ const [x1, y1, z1] = args.v1.split(',').map(Number);
+ const [x2, y2, z2] = args.v2.split(',').map(Number);
+ const [x3, y3, z3] = args.v3.split(',').map(Number);
+ const { pos } = this.camera;
+
+ // Calculate face normal
+ const ux = x2 - x1, uy = y2 - y1, uz = z2 - z1;
+ const vx = x3 - x1, vy = y3 - y1, vz = z3 - z1;
+ const nx = uy * vz - uz * vy;
+ const ny = uz * vx - ux * vz;
+ const nz = ux * vy - uy * vx;
+
+ // Vector from camera to one vertex
+ const cx = x1 - pos.x, cy = y1 - pos.y, cz = z1 - pos.z;
+
+ // Dot product of face normal and camera vector
+ const dot = nx * cx + ny * cy + nz * cz;
+
+ return dot >= 0;
+ }
+
+ // Utility Functions
+ _vectorOp(v1, v2, op) {
+ const vec1 = v1.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const vec2 = v2.split(',').map(n => Scratch.Cast.toNumber(n) || 0);
+ const [x1 = 0, y1 = 0, z1 = 0] = vec1;
+ const [x2 = 0, y2 = 0, z2 = 0] = vec2;
+ return `${op(x1, x2)},${op(y1, y2)},${op(z1, z2)}`;
+ }
+
+ _scalarOp(v, scalar, op) {
+ const [x, y, z] = v.split(',').map(Number);
+ return `${op(x, scalar)},${op(y, scalar)},${op(z, scalar)}`;
+ }
+
+ _setVec(target, v) { const [x, y, z] = v.split(',').map(Number); target.x = x; target.y = y; target.z = z; }
+ _addVec(target, v) { const [x, y, z] = v.split(',').map(Number); target.x += x; target.y += y; target.z += z; }
+ _setRot(target, v) {
+ const [yaw, pitch, roll] = v.split(',').map(Number);
+ target.yaw = yaw;
+ target.pitch = pitch;
+ target.roll = roll;
+ }
+
+ _addRot(target, v) {
+ const [yaw, pitch, roll] = v.split(',').map(Number);
+ target.yaw += yaw;
+ target.pitch += pitch;
+ target.roll += roll;
+ }
+
+ _rotate3D(v, rx, ry, rz) {
+ const cos = Math.cos;
+ const sin = Math.sin;
+ let { x, y, z } = v;
+
+ // Apply rotations in order: X -> Y -> Z
+ // X rotation
+ let temp = y;
+ y = y * cos(rx) - z * sin(rx);
+ z = temp * sin(rx) + z * cos(rx);
+
+ // Y rotation
+ temp = x;
+ x = x * cos(ry) + z * sin(ry);
+ z = -temp * sin(ry) + z * cos(ry);
+
+ // Z rotation
+ temp = x;
+ x = x * cos(rz) - y * sin(rz);
+ y = temp * sin(rz) + y * cos(rz);
+
+ return `${x},${y},${z}`;
+ }
+
+ moveByVec2(args, util) {
+ const [dx, dy] = args.v.split(',').map(Number);
+ const x = Scratch.Cast.toNumber(dx);
+ const y = Scratch.Cast.toNumber(dy);
+ util.target.setXY(util.target.x + x, util.target.y + y);
+ }
+
+ pointTo(args, util) {
+ const [dx, dy] = args.v.split(',').map(Number);
+ const x = Scratch.Cast.toNumber(dx);
+ const y = Scratch.Cast.toNumber(dy);
+ if (util.target.y > y) {
+ util.target.setDirection(
+ (180 / Math.PI) *
+ Math.atan((x - util.target.x) / (y - util.target.y)) +
+ 180
+ );
+ } else {
+ util.target.setDirection(
+ (180 / Math.PI) * Math.atan((x - util.target.x) / (y - util.target.y))
+ );
+ }
+ }
+
+ goToVec2(args, util) {
+ const [dx, dy] = args.v.split(',').map(Number);
+ const x = Scratch.Cast.toNumber(dx);
+ const y = Scratch.Cast.toNumber(dy);
+ util.target.setXY(x, y);
+ }
+
+ getPosVec2(util) {
+ return `${util.target.x},${util.target.y}`;
+ }
+
+ getSizeVec2(util) {
+ const bounds = Scratch.vm.renderer.getBounds(util.target.drawableID)
+ return `${Math.ceil(bounds.width)},${Math.ceil(bounds.height)}`;
+ }
+ getMousePosVec2() {
+ return `${Math.round(mouse._scratchX)},${Math.round(mouse._scratchY)}`;
+ }
+
+ drawTriangle(args, util) {
+ const [x1, y1] = args.v1.split(',').map(Number);
+ const [x2, y2] = args.v2.split(',').map(Number);
+ const [x3, y3] = args.v3.split(',').map(Number);
+
+ penPModule.drawSolidTri(
+ {
+ x1: x1,
+ y1: y1,
+ x2: x2,
+ y2: y2,
+ x3: x3,
+ y3: y3,
+ },
+ util
+ );
+ }
+
+ // New utility function to convert degrees to radians
+ _toRadians(degrees) {
+ return (degrees || 0) * (Math.PI / 180);
+ }
+ }
+
+ Scratch.extensions.register(new VecMath());
+})(Scratch);
diff --git a/static/images/Cool_skratcher/vectors.svg b/static/images/Cool_skratcher/vectors.svg
new file mode 100644
index 00000000..bc388ede
--- /dev/null
+++ b/static/images/Cool_skratcher/vectors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file