diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ed35756 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Release + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + release-ubuntu: + name: Release on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install ubuntu Dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake g++-14 ninja-build libasound2-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev libxcursor-dev libxinerama-dev + - name: Configure CMake + run: cmake -B ${{ github.workspace }}/build -S . -DCMAKE_BUILD_TYPE=Release + env: + CXX: g++-14 + - name: Build Target + run: cmake --build ${{ github.workspace }}/build --config Release + - name: Package Asset + run: tar -czvf vectorjs-linux.tar.gz -C ${{ github.workspace }}/build vectorjs + - name: Upload Binary to Release + uses: softprops/action-gh-release@v2 + with: + files: vectorjs-linux.tar.gz + + release-windows: + name: Release on Windows + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Configure CMake + run: cmake -B ${{ github.workspace }}\build -S . -DCMAKE_BUILD_TYPE=Release + - name: Build Target + run: cmake --build ${{ github.workspace }}\build --config Release + - name: Package Asset + shell: pwsh + run: Compress-Archive -Path "${{ github.workspace }}\build\Release\vectorjs.exe" -DestinationPath "vectorjs-windows.zip" + - name: Upload Binary to Release + uses: softprops/action-gh-release@v2 + with: + files: vectorjs-windows.zip + + release-mac: + name: Release on macOS + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install Ninja + run: brew install ninja + - name: Configure CMake + run: cmake -B ${{ github.workspace }}/build -S . -DCMAKE_BUILD_TYPE=Release + - name: Build Target + run: cmake --build ${{ github.workspace }}/build --config Release + - name: Package Asset + run: tar -czvf vectorjs-macos.tar.gz -C ${{ github.workspace }}/build vectorjs + - name: Upload Binary to Release + uses: softprops/action-gh-release@v2 + with: + files: vectorjs-macos.tar.gz \ No newline at end of file diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..dd67487 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,60 @@ +name: Verify +concurrency: + cancel-in-progress: false + group: verify + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + types: [opened, synchronize, reopened] + +jobs: + verify-ubuntu: + name: Verify on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install ubuntu Dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake g++-14 ninja-build libasound2-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev libxcursor-dev libxinerama-dev + - name: Configure CMake + run: cmake -B ${{ github.workspace }}/build -S . -DCMAKE_BUILD_TYPE=Release + env: + CXX: g++-14 + - name: Build Target + run: cmake --build ${{ github.workspace }}/build --config Release + - name: Verify Executable Production + run: ls -l ${{ github.workspace }}/build/vectorjs + + verify-windows: + name: Verify on Windows + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Configure CMake + run: cmake -B ${{ github.workspace }}\build -S . -DCMAKE_BUILD_TYPE=Release + - name: Build Target + run: cmake --build ${{ github.workspace }}\build --config Release + - name: Verify Executable Production + shell: pwsh + run: Test-Path "${{ github.workspace }}\build\Release\vectorjs.exe" + + verify-mac: + name: Verify on macOS + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install Ninja + run: brew install ninja + - name: Configure CMake + run: cmake -B ${{ github.workspace }}/build -S . -DCMAKE_BUILD_TYPE=Release + - name: Build Target + run: cmake --build ${{ github.workspace }}/build --config Release + - name: Verify Executable Production + run: ls -l ${{ github.workspace }}/build/vectorjs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..642775e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.28) +project(vectorjs VERSION 0.1.0 LANGUAGES CXX) + +# Require C++23 standard +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(FetchContent) + +option(ENABLE_MSVC_WIN_MAIN "Hide the console window for MSVC builds" OFF) + +# ----------------------------------------------------------------------------- +# 1. Dependencies +# ----------------------------------------------------------------------------- + +# CLI11 (Header-only CLI parser) +FetchContent_Declare( + cli11 + GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git + GIT_TAG v2.6.2 +) +set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + +# raylib (Windowing, input, and graphics library) +FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 6.0 +) +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BUILD_GAMES OFF CACHE BOOL "" FORCE) + +# qjs +FetchContent_Declare( + qjs + GIT_REPOSITORY https://github.com/quickjs-ng/quickjs + GIT_TAG v0.15.1 +) + +# Bring all dependencies into the build tree automatically +FetchContent_MakeAvailable(cli11 raylib qjs) + +# ----------------------------------------------------------------------------- +# 2. Executable Target +# ----------------------------------------------------------------------------- + +# Define the target executable +add_executable(vectorjs) + +# Add source files to the target +target_sources(vectorjs PRIVATE + "src/main.cpp" + "src/js_engine.cpp" + "src/core.cpp" + "src/api/api_context.cpp" + "src/api/api_classes.cpp" + "src/api/api_enums.cpp" +) + +# Hide console on Windows, keep standard main() entry point +if(MSVC AND ENABLE_MSVC_WIN_MAIN) + target_link_options(vectorjs PRIVATE + "LINKER:/SUBSYSTEM:WINDOWS" + "LINKER:/ENTRY:mainCRTStartup" + ) +endif() + +# Include directories +target_include_directories(vectorjs PRIVATE src) + +# Link dependencies +target_link_libraries(vectorjs PRIVATE + CLI11::CLI11 + raylib + qjs +) + +# ----------------------------------------------------------------------------- +# 3. Options +# ----------------------------------------------------------------------------- + +#function(add_feature_flag OPTION_NAME DESCRIPTION DEFAULT_VAL MACRO_NAME) +# option(${OPTION_NAME} "${DESCRIPTION}" ${DEFAULT_VAL}) +# +# if(${OPTION_NAME}) +# target_compile_definitions(vectorjs PRIVATE ${MACRO_NAME}=1) +# else() +# target_compile_definitions(vectorjs PRIVATE ${MACRO_NAME}=0) +# endif() +#endfunction() +# +#add_feature_flag(ENABLE_2D "Enable 2D graphics systems" ON ENABLE_FEATURE_2D) +#add_feature_flag(ENABLE_3D "Enable 3D graphics systems" ON ENABLE_FEATURE_3D) +#add_feature_flag(ENABLE_FONT "Enable fonts" OFF ENABLE_FEATURE_FONT) +#add_feature_flag(ENABLE_AUDIO "Enable audio engine" ON ENABLE_FEATURE_AUDIO) + diff --git a/README.MD b/README.MD index e69de29..616ad9e 100644 --- a/README.MD +++ b/README.MD @@ -0,0 +1,41 @@ +# VectorJS + +## Dependencies +* Raylib +* Quickjs-ng + +## Example +```js +import {Application, Rectangle, Vector2, Palette } from "vectorjs"; + +const screenWidth = 800; +const screenHeight = 600; +const fpsPos = new Vector2(10, 10); +const rect = new Rectangle(30, 30, 200, 45); +const point1 = new Vector2(400, 150); +const point2 = new Vector2(300, 350); +const point3 = new Vector2(500, 350); + +const app = new Application(screenWidth, screenHeight, "Window"); +app.run({ + + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.drawFPS(fpsPos); + + ctx.shapes.drawRectangle(rect, { + color: Palette.RED + }) + + ctx.shapes.drawCircle(point1, 60, { + color: Palette.GREEN + }) + + ctx.shapes.drawTriangle(point1, point2, point3, { + color: Palette.BLUE + }) + + }); + } +}); +``` \ No newline at end of file diff --git a/examples/2d/clock.js b/examples/2d/clock.js new file mode 100644 index 0000000..b0dea74 --- /dev/null +++ b/examples/2d/clock.js @@ -0,0 +1,91 @@ +import { Application, Vector2, Palette } from "vectorjs"; + +// Window configuration +const screenWidth = 600; +const screenHeight = 600; +const center = new Vector2(screenWidth / 2, screenHeight / 2); +const fpsPos = new Vector2(10, 10); + +// Clock dimensions +const clockRadius = 200; +const hourHandLength = 100; +const minuteHandLength = 140; +const secondHandLength = 160; + +const app = new Application(screenWidth, screenHeight, "Analog Clock"); + +app.run({ + onDraw(render) { + render.withLayer2D((ctx) => { + + // 1. Draw Clock Face Outer Ring & Center + ctx.shapes.drawCircle(center, clockRadius, { + color: Palette.WHITE + }); + ctx.shapes.drawCircle(center, 8, { + color: Palette.BLACK + }); + + // 2. Draw Hour Ticks (12 main markers) + for (let i = 0; i < 12; i++) { + const angle = (i * 30 - 90) * (Math.PI / 180); + const innerRadius = clockRadius - 20; + + const tickStart = new Vector2( + center.x + innerRadius * Math.cos(angle), + center.y + innerRadius * Math.sin(angle) + ); + const tickEnd = new Vector2( + center.x + clockRadius * Math.cos(angle), + center.y + clockRadius * Math.sin(angle) + ); + + ctx.shapes.drawLine(tickStart, tickEnd, { + color: Palette.GRAY + }); + } + + // 3. Get Current Time + const now = new Date(); + const hours = now.getHours() % 12; + const minutes = now.getMinutes(); + const seconds = now.getSeconds(); + const milliseconds = now.getMilliseconds(); + + // Smooth calculations for hand angles (in radians, -90 deg offset to start at 12 o'clock) + const secondAngle = ((seconds + milliseconds / 1000) * 6 - 90) * (Math.PI / 180); + const minuteAngle = ((minutes + seconds / 60) * 6 - 90) * (Math.PI / 180); + const hourAngle = ((hours + minutes / 60) * 30 - 90) * (Math.PI / 180); + + // Calculate hand end positions + const hourHandEnd = new Vector2( + center.x + hourHandLength * Math.cos(hourAngle), + center.y + hourHandLength * Math.sin(hourAngle) + ); + const minuteHandEnd = new Vector2( + center.x + minuteHandLength * Math.cos(minuteAngle), + center.y + minuteHandLength * Math.sin(minuteAngle) + ); + const secondHandEnd = new Vector2( + center.x + secondHandLength * Math.cos(secondAngle), + center.y + secondHandLength * Math.sin(secondAngle) + ); + + // 4. Draw Clock Hands + // Hour Hand + ctx.shapes.drawLine(center, hourHandEnd, { + color: Palette.BLUE + }); + + // Minute Hand + ctx.shapes.drawLine(center, minuteHandEnd, { + color: Palette.GREEN + }); + + // Second Hand + ctx.shapes.drawLine(center, secondHandEnd, { + color: Palette.RED + }); + }); + } +}); \ No newline at end of file diff --git a/examples/2d/shapes.js b/examples/2d/shapes.js new file mode 100644 index 0000000..242f4d4 --- /dev/null +++ b/examples/2d/shapes.js @@ -0,0 +1,41 @@ +import {Application, Rectangle, Vector2, Palette } from "vectorjs"; + +const screenWidth = 600; +const screenHeight = 800; +const fpsPos = new Vector2(10, 10); +const rect = new Rectangle(30, 30, 200, 45); +const point1 = new Vector2(400, 150); +const point2 = new Vector2(300, 350); +const point3 = new Vector2(500, 350); +const point4 = new Vector2(700, 500); + +const app = new Application(screenWidth, screenHeight,"Window"); +app.run({ + + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.drawFPS(fpsPos); + + ctx.shapes.drawRectangle(rect, { + color: Palette.RED + }) + + ctx.shapes.drawCircle(point1, 60, { + color: Palette.GREEN + }) + + ctx.shapes.drawTriangle(point1, point2, point3, { + color: Palette.BLUE + }) + + ctx.shapes.drawEllipse(point4, 75.0, 50.0, { + color: Palette.ORANGE + }) + + ctx.shapes.drawLine(point4, point1, { + color: Palette.BROWN + }); + + }); + } +}); \ No newline at end of file diff --git a/examples/2d/shapes2.js b/examples/2d/shapes2.js new file mode 100644 index 0000000..91184b6 --- /dev/null +++ b/examples/2d/shapes2.js @@ -0,0 +1,78 @@ +import { Application, Vector2, Palette } from "vectorjs"; + +const screenWidth = 800; +const screenHeight = 600; +const center = new Vector2(screenWidth / 2, screenHeight / 2); +const fpsPos = new Vector2(10, 10); + +const app = new Application(screenWidth, screenHeight, "Animated Shapes"); + +app.run({ + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.drawFPS(fpsPos); + + // Time variable driving all animations + const time = Date.now() * 0.003; + + // 1. Pulsing Center Circle + const pulseRadius = 50 + Math.sin(time * 2) * 20; + ctx.shapes.drawCircle(center, pulseRadius, { + color: Palette.RED + }); + + // 2. Bouncing Ball (Vertical Sinusoidal Motion) + const bounceY = center.y + Math.abs(Math.sin(time * 3)) * -200 + 100; + const bouncePos = new Vector2(150, bounceY); + ctx.shapes.drawCircle(bouncePos, 30, { + color: Palette.ORANGE + }); + + // 3. Orbiting Satellite (Circular Path) + const orbitRadius = 180; + const orbitPos = new Vector2( + center.x + Math.cos(time) * orbitRadius, + center.y + Math.sin(time) * orbitRadius + ); + + // Connection line to center + ctx.shapes.drawLine(center, orbitPos, { + color: Palette.GRAY + }); + + ctx.shapes.drawCircle(orbitPos, 15, { + color: Palette.BLUE + }); + + // 4. Rotating Triangle Vertices + const triangleRadius = 80; + const triangleCenter = new Vector2(650, 200); + + const p1 = new Vector2( + triangleCenter.x + Math.cos(time) * triangleRadius, + triangleCenter.y + Math.sin(time) * triangleRadius + ); + const p2 = new Vector2( + triangleCenter.x + Math.cos(time + (2 * Math.PI / 3)) * triangleRadius, + triangleCenter.y + Math.sin(time + (2 * Math.PI / 3)) * triangleRadius + ); + const p3 = new Vector2( + triangleCenter.x + Math.cos(time + (4 * Math.PI / 3)) * triangleRadius, + triangleCenter.y + Math.sin(time + (4 * Math.PI / 3)) * triangleRadius + ); + + ctx.shapes.drawTriangle(p1, p2, p3, { + color: Palette.GREEN + }); + + // 5. Morphing Ellipse (Width & Height Oscillations) + const ellipsePos = new Vector2(650, 450); + const radiusX = 60 + Math.cos(time * 2) * 30; + const radiusY = 60 + Math.sin(time * 2) * 30; + + ctx.shapes.drawEllipse(ellipsePos, radiusX, radiusY, { + color: Palette.BROWN + }); + }); + } +}); \ No newline at end of file diff --git a/examples/2d/solar2d.js b/examples/2d/solar2d.js new file mode 100644 index 0000000..5d69faa --- /dev/null +++ b/examples/2d/solar2d.js @@ -0,0 +1,48 @@ +import { Application, Vector2, Palette } from "vectorjs"; + +const screenWidth = 800; +const screenHeight = 800; +const sunPos = new Vector2(screenWidth / 2, screenHeight / 2); +const fpsPos = new Vector2(10, 10); + +// Define planets with varied speeds and orbital radii +const planets = [ + { name: "Mercury", radius: 70, size: 6, speed: 2.5, color: Palette.GRAY }, + { name: "Venus", radius: 110, size: 10, speed: 1.8, color: Palette.ORANGE }, + { name: "Earth", radius: 170, size: 12, speed: 1.2, color: Palette.BLUE }, + { name: "Mars", radius: 240, size: 9, speed: 0.9, color: Palette.RED }, + { name: "Jupiter", radius: 320, size: 22, speed: 0.5, color: Palette.BROWN } +]; + +const app = new Application(screenWidth, screenHeight, "Solar System Simulation"); + +app.run({ + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.drawFPS(fpsPos); + + const time = Date.now() * 0.001; + + // 1. Draw the central Sun + ctx.shapes.drawCircle(sunPos, 35, { + color: Palette.YELLOW + }); + + // 2. Render each planet along its orbital path + planets.forEach((planet) => { + + // Calculate planet position based on its distinct speed + const angle = time * planet.speed; + const planetPos = new Vector2( + sunPos.x + Math.cos(angle) * planet.radius, + sunPos.y + Math.sin(angle) * planet.radius + ); + + // Draw planet + ctx.shapes.drawCircle(planetPos, planet.size, { + color: planet.color + }); + }); + }); + } +}); \ No newline at end of file diff --git a/examples/assets/AnonymousPro-Regular.ttf b/examples/assets/AnonymousPro-Regular.ttf new file mode 100644 index 0000000..a98da85 Binary files /dev/null and b/examples/assets/AnonymousPro-Regular.ttf differ diff --git a/examples/assets/sample.png b/examples/assets/sample.png new file mode 100644 index 0000000..12d841c Binary files /dev/null and b/examples/assets/sample.png differ diff --git a/examples/assets/sound.wav b/examples/assets/sound.wav new file mode 100644 index 0000000..b5d01c9 Binary files /dev/null and b/examples/assets/sound.wav differ diff --git a/examples/info.js b/examples/info.js new file mode 100644 index 0000000..b0d28c8 --- /dev/null +++ b/examples/info.js @@ -0,0 +1,17 @@ +import {Application, Vector2, Palette, Info} from "vectorjs"; + +const screenWidth = 600; +const screenHeight = 800; +const line1 = new Vector2(10, 10); +const line2 = new Vector2(10, 30); + +const app = new Application(screenWidth, screenHeight,"Info"); +app.run({ + + onDraw(render) { + render.withLayer2D((ctx) => { + ctx.text.drawText(line1, `RAYLIB VERSION: ${Info.RAYLIB_VERSION}`) + ctx.text.drawText(line2, `QUICKJS VERSION: ${Info.QUICKJS_VERSION}`) + }); + } +}); \ No newline at end of file diff --git a/src/api/api_classes.cpp b/src/api/api_classes.cpp new file mode 100644 index 0000000..a80bee6 --- /dev/null +++ b/src/api/api_classes.cpp @@ -0,0 +1,195 @@ +#include "hostapi.hpp" +#include "js_types.hpp" +#include "js_utils.hpp" +#include "js_context.hpp" + +#define JS_BIND_PROP(ClassType, ClassIDPtr, name, Member) \ + JS_CGETSET_DEF( \ + name, \ + (&HostApi::Utils::js_generic_getter), \ + (&HostApi::Utils::js_generic_setter) \ + ) + +namespace HostApi { + + static void register_application_class(JSContext* ctx, JSModuleDef* m) { + static constexpr JSCFunctionListEntry proto_funcs[] = { + JS_CFUNC_DEF("run", 1, [](JSContext* c, JSValueConst this_val, int argc, JSValueConst* argv) -> JSValue { + const auto* app = Utils::get_opaque(c, this_val, js_application_class_id); + return app ? app->Run(c, this_val, argc, argv) + : JS_ThrowTypeError(c, "Invalid JSApplication instance"); + }) + }; + + Utils::register_js_class(ctx, m, { + .name = "Application", + .class_id = js_application_class_id, + .finalizer = [](auto*, JSValue val) { + delete Utils::get_opaque(val, js_application_class_id); + }, + .constructor = [](auto c, auto new_target, const int argc, auto argv) -> JSValue { + int32_t h = 0, w = 0; + if (argc > 0 && JS_ToInt32(c, &h, argv[0]) < 0) return JS_EXCEPTION; + if (argc > 1 && JS_ToInt32(c, &w, argv[1]) < 0) return JS_EXCEPTION; + std::string title = argc > 2 ? Utils::js_to_std_string(c, argv[2]) : "VectorJS Application"; + return Utils::create_js_instance(c, new_target, js_application_class_id, h, w, std::move(title)); + }, + .proto_funcs = proto_funcs + }); + } + + JSApplication::JSApplication(const int w, const int h, const std::string& title) { + InitWindow(w, h, title.c_str()); + SetTargetFPS(60); + } + + JSValue JSApplication::Run(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { + if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a user application config object"); + JSValueConst user_app = argv[0]; + + Utils::ScopedJSValue on_init_func(ctx, JS_GetPropertyStr(ctx, user_app, "onInit")); + Utils::ScopedJSValue on_update_func(ctx, JS_GetPropertyStr(ctx, user_app, "onUpdate")); + Utils::ScopedJSValue on_draw_func(ctx, JS_GetPropertyStr(ctx, user_app, "onDraw")); + + // Instantiate rendering and context objects ONCE outside the frame loop + Utils::ScopedJSValue update_obj(ctx, create_update_context_object(ctx)); + Utils::ScopedJSValue render_obj(ctx, create_draw_render_object(ctx)); + + if (JS_IsFunction(ctx, on_init_func)) { + Utils::ScopedJSValue ret(ctx, JS_Call(ctx, on_init_func, user_app, 0, nullptr)); + if (JS_IsException(ret)) return JS_EXCEPTION; + } + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(RAYWHITE); + + if (JS_IsFunction(ctx, on_update_func)) { + JSValue u = update_obj.get(); + Utils::ScopedJSValue ret(ctx, JS_Call(ctx, on_update_func, user_app, 1, &u)); + if (JS_IsException(ret.get())) { + EndDrawing(); + if (IsWindowReady()) CloseWindow(); + return JS_EXCEPTION; + } + } + if (JS_IsFunction(ctx, on_draw_func)) { + JSValue r = render_obj.get(); + Utils::ScopedJSValue ret(ctx, JS_Call(ctx, on_draw_func, user_app, 1, &r)); + if (JS_IsException(ret.get())) { + EndDrawing(); + if (IsWindowReady()) CloseWindow(); + return JS_EXCEPTION; + } + } + EndDrawing(); + } + + if (IsWindowReady()) { + CloseWindow(); + } + + return JS_UNDEFINED; + } + + static void register_color_class(JSContext* ctx, JSModuleDef* m) { + static constexpr JSCFunctionListEntry proto_funcs[] = { + JS_BIND_PROP(JSColor, &js_color_class_id, "r", r), + JS_BIND_PROP(JSColor, &js_color_class_id, "g", g), + JS_BIND_PROP(JSColor, &js_color_class_id, "b", b), + JS_BIND_PROP(JSColor, &js_color_class_id, "a", a), + }; + + Utils::register_js_class(ctx, m, { + .name = "Color", + .class_id = js_color_class_id, + .finalizer = [](auto, JSValue val) { + delete Utils::get_opaque(val, js_color_class_id); + }, + .constructor = [](auto c, auto new_target, int argc, auto argv) -> JSValue { + if (argc >= 4) { + int32_t r = 0, g = 0, b = 0, a = 255; + if (argc > 0 && JS_ToInt32(c, &r, argv[0]) < 0) return JS_EXCEPTION; + if (argc > 1 && JS_ToInt32(c, &g, argv[1]) < 0) return JS_EXCEPTION; + if (argc > 2 && JS_ToInt32(c, &b, argv[2]) < 0) return JS_EXCEPTION; + if (argc > 3 && JS_ToInt32(c, &a, argv[3]) < 0) return JS_EXCEPTION; + + return Utils::create_js_instance( + c, new_target, js_color_class_id, + static_cast(r), static_cast(g), + static_cast(b), static_cast(a) + ); + } + return Utils::create_js_instance(c, new_target, js_color_class_id); + }, + .proto_funcs = proto_funcs + }); + } + + static void register_vector2_class(JSContext* ctx, JSModuleDef* m) { + static constexpr JSCFunctionListEntry proto_funcs[] = { + JS_BIND_PROP(JSVector2, &js_vector2_class_id, "x", x), + JS_BIND_PROP(JSVector2, &js_vector2_class_id, "y", y), + }; + + Utils::register_js_class(ctx, m, { + .name = "Vector2", + .class_id = js_vector2_class_id, + .finalizer = [](auto, JSValue val) { + delete Utils::get_opaque(val, js_vector2_class_id); + }, + .constructor = [](auto c, auto new_target, int argc, auto argv) -> JSValue { + double x = 0, y = 0; + if (argc > 0 && JS_ToFloat64(c, &x, argv[0]) < 0) return JS_EXCEPTION; + if (argc > 1 && JS_ToFloat64(c, &y, argv[1]) < 0) return JS_EXCEPTION; + + return Utils::create_js_instance( + c, new_target, js_vector2_class_id, + static_cast(x), static_cast(y) + ); + }, + .proto_funcs = proto_funcs + }); + } + + static void register_rectangle_class(JSContext* ctx, JSModuleDef* m) { + static constexpr JSCFunctionListEntry proto_funcs[] = { + JS_BIND_PROP(JSRectangle, &js_rectangle_class_id, "x", x), + JS_BIND_PROP(JSRectangle, &js_rectangle_class_id, "y", y), + JS_BIND_PROP(JSRectangle, &js_rectangle_class_id, "width", width), + JS_BIND_PROP(JSRectangle, &js_rectangle_class_id, "height", height), + }; + + Utils::register_js_class(ctx, m, { + .name = "Rectangle", + .class_id = js_rectangle_class_id, + .finalizer = [](auto, JSValue val) { + delete Utils::get_opaque(val, js_rectangle_class_id); + }, + .constructor = [](auto c, auto new_target, int argc, auto argv) -> JSValue { + double x = 0, y = 0, w = 0, h = 0; + if (argc > 0 && JS_ToFloat64(c, &x, argv[0]) < 0) return JS_EXCEPTION; + if (argc > 1 && JS_ToFloat64(c, &y, argv[1]) < 0) return JS_EXCEPTION; + if (argc > 2 && JS_ToFloat64(c, &w, argv[2]) < 0) return JS_EXCEPTION; + if (argc > 3 && JS_ToFloat64(c, &h, argv[3]) < 0) return JS_EXCEPTION; + + return Utils::create_js_instance( + c, new_target, js_rectangle_class_id, + static_cast(x), static_cast(y), + static_cast(w), static_cast(h) + ); + }, + .proto_funcs = proto_funcs + }); + } + + void register_hapi_classes(JSContext* ctx, JSModuleDef* m) { + register_application_class(ctx, m); + register_color_class(ctx, m); + register_vector2_class(ctx, m); + register_rectangle_class(ctx, m); + } + +} // namespace HostApi + +#undef JS_BIND_PROP \ No newline at end of file diff --git a/src/api/api_context.cpp b/src/api/api_context.cpp new file mode 100644 index 0000000..2f18a5c --- /dev/null +++ b/src/api/api_context.cpp @@ -0,0 +1,114 @@ +#include "hostapi.hpp" +#include "js_types.hpp" +#include "js_utils.hpp" +#include + +namespace HostApi { + + static JSDrawOptions parse_draw_options(JSContext* ctx, JSValueConst optionsObj) { + JSDrawOptions options; + if (JS_IsObject(optionsObj)) { + Utils::try_get_opaque_property(ctx, optionsObj, "color", js_color_class_id, options.color); + Utils::try_get_opaque_property(ctx, optionsObj, "origin", js_color_class_id, options.origin); + Utils::try_get_opaque_property(ctx, optionsObj, "rotation", js_color_class_id, options.rotation); + Utils::try_get_opaque_property(ctx, optionsObj, "wireframe", js_color_class_id, options.wireframe); + } + return options; + } + + JSValue create_update_context_object(JSContext* ctx) { + return JS_NewObject(ctx); + } + + JSValue create_draw_render_object(JSContext* ctx) { + Utils::ScopedJSValue render2d_obj(ctx, JS_NewObject(ctx)); + + // --- FPS Binding --- + JS_SetPropertyStr(ctx, render2d_obj, "drawFPS", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 1) return JS_ThrowTypeError(c, "drawFPS requires a Vector2 position argument"); + const auto pos = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + ::DrawFPS(static_cast(pos.x), static_cast(pos.y)); + return JS_UNDEFINED; + }, "drawFPS", 1)); + + // --- Shapes Sub-Object --- + Utils::ScopedJSValue shape_obj(ctx, JS_NewObject(ctx)); + + JS_SetPropertyStr(ctx, shape_obj, "drawPixel", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 2) return JS_ThrowTypeError(c, "drawPixel requires position and color arguments"); + const auto pos = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + const auto col = Utils::get_opaque_or(c, argv[1], js_color_class_id, JSColor(RAYWHITE)); + ::DrawPixelV(pos, col); + return JS_UNDEFINED; + }, "drawPixel", 2)); + + JS_SetPropertyStr(ctx, shape_obj, "drawLine", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 2) return JS_ThrowTypeError(c, "drawLine requires start and end positions"); + const auto start = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + const auto end = Utils::get_opaque_or(c, argv[1], js_vector2_class_id, {0, 0}); + const JSDrawOptions draw_options = parse_draw_options(c, argc > 2 ? argv[2] : JS_UNDEFINED); + ::DrawLineV(start, end, draw_options.color); + return JS_UNDEFINED; + }, "drawLine", 3)); + + JS_SetPropertyStr(ctx, shape_obj, "drawRectangle", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 1) return JS_ThrowTypeError(c, "drawRectangle requires a Rectangle argument"); + const auto rect = Utils::get_opaque_or(c, argv[0], js_rectangle_class_id, {0, 0, 0, 0}); + const JSDrawOptions draw_options = parse_draw_options(c, argc > 1 ? argv[1] : JS_UNDEFINED); + ::DrawRectanglePro(rect, draw_options.origin, draw_options.rotation, draw_options.color); + return JS_UNDEFINED; + }, "drawRectangle", 2)); + + JS_SetPropertyStr(ctx, shape_obj, "drawCircle", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 2) return JS_ThrowTypeError(c, "drawCircle requires center and radius arguments"); + const auto center = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + double rad = 0; + if (JS_ToFloat64(c, &rad, argv[1]) != 0) { + return JS_ThrowTypeError(c, "drawCircle radius must be a number"); + } + const JSDrawOptions draw_options = parse_draw_options(c, argc > 2 ? argv[2] : JS_UNDEFINED); + ::DrawCircleV(center, static_cast(rad), draw_options.color); + return JS_UNDEFINED; + }, "drawCircle", 3)); + + JS_SetPropertyStr(ctx, shape_obj, "drawTriangle", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 3) return JS_ThrowTypeError(c, "drawTriangle requires 3 Vector2 point arguments"); + const auto p1 = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + const auto p2 = Utils::get_opaque_or(c, argv[1], js_vector2_class_id, {0, 0}); + const auto p3 = Utils::get_opaque_or(c, argv[2], js_vector2_class_id, {0, 0}); + const JSDrawOptions draw_options = parse_draw_options(c, argc > 3 ? argv[3] : JS_UNDEFINED); + ::DrawTriangle(p1, p2, p3, draw_options.color); + return JS_UNDEFINED; + }, "drawTriangle", 4)); + + JS_SetPropertyStr(ctx, shape_obj, "drawEllipse", JS_NewCFunction(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv) -> JSValue { + if (argc < 3) return JS_ThrowTypeError(c, "drawEllipse requires center, radiusH, and radiusV arguments"); + const auto center = Utils::get_opaque_or(c, argv[0], js_vector2_class_id, {0, 0}); + double radH = 0, radV = 0; + if (JS_ToFloat64(c, &radH, argv[1]) != 0 || JS_ToFloat64(c, &radV, argv[2]) != 0) { + return JS_ThrowTypeError(c, "drawEllipse radii parameters must be numbers"); + } + const JSDrawOptions draw_options = parse_draw_options(c, argc > 3 ? argv[3] : JS_UNDEFINED); + ::DrawEllipseV(center, static_cast(radH), static_cast(radV), draw_options.color); + return JS_UNDEFINED; + }, "drawEllipse", 4)); + + JS_SetPropertyStr(ctx, render2d_obj, "shapes", shape_obj.release()); + + // --- Layer Wrappers --- + const JSValue render_obj = JS_NewObject(ctx); + JSValue r2d_val = render2d_obj.release(); + + JS_SetPropertyStr(ctx, render_obj, "withLayer2D", JS_NewCFunctionData(ctx, [](JSContext* c, JSValueConst, int argc, JSValueConst* argv, int, JSValue* magic_argv) -> JSValue { + if (argc > 0 && JS_IsFunction(c, argv[0])) { + const Utils::ScopedJSValue res(c, JS_Call(c, argv[0], JS_UNDEFINED, 1, &magic_argv[0])); + if (JS_IsException(res.get())) return JS_EXCEPTION; + } + return JS_UNDEFINED; + }, 1, 0, 1, &r2d_val)); + + JS_FreeValue(ctx, r2d_val); + return render_obj; + } + +} // namespace HostApi \ No newline at end of file diff --git a/src/api/api_enums.cpp b/src/api/api_enums.cpp new file mode 100644 index 0000000..5182806 --- /dev/null +++ b/src/api/api_enums.cpp @@ -0,0 +1,76 @@ +#include "hostapi.hpp" +#include "js_utils.hpp" +#include + +namespace HostApi { + +void register_hapi_enums(JSContext* ctx, JSModuleDef* m) { + + static auto export_object = [](JSContext* js_context, JSModuleDef* js_module_def, const char* obj_name, Pairs&&... pairs) { + const JSValue obj = JS_NewObject(js_context); + Utils::set_object_properties(js_context, obj, std::forward(pairs)...); + JS_SetModuleExport(js_context, js_module_def, obj_name, obj); + }; + + constexpr static auto P = [](const char* name, V&& val) { + return std::pair{name, std::forward(val)}; + }; + + // --- Palette Object --- + export_object(ctx, m, "Palette", + P("LIGHTGRAY", LIGHTGRAY), + P("GRAY", GRAY), + P("DARKGRAY", DARKGRAY), + P("YELLOW", YELLOW), + P("GOLD", GOLD), + P("ORANGE", ORANGE), + P("PINK", PINK), + P("RED", RED), + P("MAROON", MAROON), + P("GREEN", GREEN), + P("LIME", LIME), + P("DARKGREEN", DARKGREEN), + P("SKYBLUE", SKYBLUE), + P("BLUE", BLUE), + P("DARKBLUE", DARKBLUE), + P("PURPLE", PURPLE), + P("VIOLET", VIOLET), + P("DARKPURPLE", DARKPURPLE), + P("BEIGE", BEIGE), + P("BROWN", BROWN), + P("DARKBROWN", DARKBROWN), + P("WHITE", WHITE), + P("BLACK", BLACK), + P("BLANK", BLANK), + P("MAGENTA", MAGENTA), + P("RAYWHITE", RAYWHITE) + ); + + // --- Info Object --- + export_object(ctx, m, "Info", + P("RAYLIB_VERSION", RAYLIB_VERSION_STR), + P("QUICKJS_VERSION", QUICKJS_VERSION_STR) + ); + + // --- ConfigFlags Object --- + export_object(ctx, m, "ConfigFlags", + P("FLAG_VSYNC_HINT", FLAG_VSYNC_HINT), + P("FLAG_FULLSCREEN_MODE", FLAG_FULLSCREEN_MODE), + P("FLAG_WINDOW_RESIZABLE", FLAG_WINDOW_RESIZABLE), + P("FLAG_WINDOW_UNDECORATED", FLAG_WINDOW_UNDECORATED), + P("FLAG_WINDOW_HIDDEN", FLAG_WINDOW_HIDDEN), + P("FLAG_WINDOW_MINIMIZED", FLAG_WINDOW_MINIMIZED), + P("FLAG_WINDOW_MAXIMIZED", FLAG_WINDOW_MAXIMIZED), + P("FLAG_WINDOW_UNFOCUSED", FLAG_WINDOW_UNFOCUSED), + P("FLAG_WINDOW_TOPMOST", FLAG_WINDOW_TOPMOST), + P("FLAG_WINDOW_ALWAYS_RUN", FLAG_WINDOW_ALWAYS_RUN), + P("FLAG_WINDOW_TRANSPARENT", FLAG_WINDOW_TRANSPARENT), + P("FLAG_WINDOW_HIGHDPI", FLAG_WINDOW_HIGHDPI), + P("FLAG_WINDOW_MOUSE_PASSTHROUGH", FLAG_WINDOW_MOUSE_PASSTHROUGH), + P("FLAG_BORDERLESS_WINDOWED_MODE", FLAG_BORDERLESS_WINDOWED_MODE), + P("FLAG_MSAA_4X_HINT", FLAG_MSAA_4X_HINT), + P("FLAG_INTERLACED_HINT", FLAG_INTERLACED_HINT) + ); +} + +} // namespace HostApi \ No newline at end of file diff --git a/src/api/hostapi.hpp b/src/api/hostapi.hpp new file mode 100644 index 0000000..f02d65e --- /dev/null +++ b/src/api/hostapi.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +namespace HostApi { + inline const std::string RAYLIB_VERSION_STR = std::format("{}.{}.{}", RAYLIB_VERSION_MAJOR, RAYLIB_VERSION_MINOR, RAYLIB_VERSION_PATCH); + inline const std::string QUICKJS_VERSION_STR = std::format("{}.{}.{}", QJS_VERSION_MAJOR, QJS_VERSION_MINOR, QJS_VERSION_PATCH); + + void register_hapi_enums(JSContext* ctx, JSModuleDef* m); + void register_hapi_classes(JSContext* ctx, JSModuleDef* m); + + inline void make_vectorjs_module(JSContext* ctx) { + JSModuleDef* m = JS_NewCModule(ctx, "vectorjs", [](JSContext *c, JSModuleDef *m) -> int { + register_hapi_classes(c, m); + register_hapi_enums(c, m); + return 0; + }); + + if (m) { + JS_AddModuleExport(ctx, m, "Application"); + JS_AddModuleExport(ctx, m, "Color"); + JS_AddModuleExport(ctx, m, "Vector2"); + JS_AddModuleExport(ctx, m, "Rectangle"); + JS_AddModuleExport(ctx, m, "Palette"); + JS_AddModuleExport(ctx, m, "Info"); + JS_AddModuleExport(ctx, m, "ConfigFlags"); + } + } +} // namespace HostApi \ No newline at end of file diff --git a/src/api/js_context.hpp b/src/api/js_context.hpp new file mode 100644 index 0000000..1507a97 --- /dev/null +++ b/src/api/js_context.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace HostApi { + JSValue create_update_context_object(JSContext* ctx); + JSValue create_draw_render_object(JSContext* ctx); +} \ No newline at end of file diff --git a/src/api/js_types.hpp b/src/api/js_types.hpp new file mode 100644 index 0000000..eea77da --- /dev/null +++ b/src/api/js_types.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include "quickjs.h" + +namespace HostApi { + + inline JSClassID js_color_class_id; + inline JSClassID js_vector2_class_id; + inline JSClassID js_rectangle_class_id; + inline JSClassID js_application_class_id; + + struct JSApplication { + JSApplication(int w, int h, const std::string& title); + static JSValue Run(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv); + }; + + struct JSColor { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 0; + JSColor() = default; + JSColor(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) : r(r), g(g), b(b), a(a) {} + explicit constexpr JSColor(const Color color) : r(color.r), g(color.g), b(color.b), a(color.a) {} + [[nodiscard]] constexpr operator Color() const { return Color{ r, g, b, a }; } + }; + + struct JSVector2 { + float x = 0.0f; + float y = 0.0f; + JSVector2() = default; + JSVector2(const float x, const float y) : x(x), y(y) {} + explicit constexpr JSVector2(const Vector2 v) : x(v.x), y(v.y) {} + [[nodiscard]] constexpr operator Vector2() const { return Vector2 { x, y }; } + }; + + struct JSRectangle { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + JSRectangle() = default; + JSRectangle(const float x, const float y, const float width, const float height) : x(x), y(y), width(width), height(height) {} + explicit constexpr JSRectangle(const Rectangle r) : x(r.x), y(r.y), width(r.width), height(r.height) {} + [[nodiscard]] constexpr operator Rectangle() const { return Rectangle { x, y, width, height }; } + }; + + struct JSDrawOptions { + JSColor color = JSColor(BLACK); + float rotation = 0.0f; + bool wireframe = false; + JSVector2 origin = JSVector2(0, 0); + }; + +} \ No newline at end of file diff --git a/src/api/js_utils.hpp b/src/api/js_utils.hpp new file mode 100644 index 0000000..9d2e292 --- /dev/null +++ b/src/api/js_utils.hpp @@ -0,0 +1,233 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "js_types.hpp" + +namespace HostApi::Utils { + + class ScopedJSValue { + public: + ScopedJSValue(JSContext* ctx, JSValue val) : ctx_(ctx), val_(val) {} + ~ScopedJSValue() { + if (ctx_ && !JS_IsUndefined(val_)) { + JS_FreeValue(ctx_, val_); + } + } + + ScopedJSValue(const ScopedJSValue&) = delete; + ScopedJSValue& operator=(const ScopedJSValue&) = delete; + + ScopedJSValue(ScopedJSValue&& other) noexcept + : ctx_(std::exchange(other.ctx_, nullptr)), val_(std::exchange(other.val_, JS_UNDEFINED)) {} + + ScopedJSValue& operator=(ScopedJSValue&& other) noexcept { + if (this != &other) { + if (ctx_ && !JS_IsUndefined(val_)) JS_FreeValue(ctx_, val_); + ctx_ = std::exchange(other.ctx_, nullptr); + val_ = std::exchange(other.val_, JS_UNDEFINED); + } + return *this; + } + + [[nodiscard]] JSValue get() const { return val_; } + [[nodiscard]] JSValue release() { + JSValue temp = val_; + val_ = JS_UNDEFINED; + return temp; + } + operator JSValue() const { return val_; } + + private: + JSContext* ctx_{nullptr}; + JSValue val_{JS_UNDEFINED}; + }; + + inline std::string js_to_std_string(JSContext* ctx, JSValueConst val, const std::string& fallback = "") { + const char* str = JS_ToCString(ctx, val); + if (!str) return fallback; + std::string result(str); + JS_FreeCString(ctx, str); + return result; + } + + template + T* get_opaque(JSContext* ctx, JSValueConst val, JSClassID class_id) { + if (JS_IsUndefined(val) || JS_IsNull(val)) return nullptr; + return static_cast(JS_GetOpaque2(ctx, val, class_id)); + } + + template + T* get_opaque(JSValueConst val, JSClassID class_id) { + return static_cast(JS_GetOpaque(val, class_id)); + } + + template + T get_opaque_or(JSContext* ctx, JSValueConst val, JSClassID class_id, const T& default_val) { + if (auto* ptr = get_opaque(ctx, val, class_id)) { + return *ptr; + } + return default_val; + } + + template + bool try_get_opaque_property(JSContext* ctx, JSValueConst obj, const char* prop_name, JSClassID class_id, T& out_val) { + if (!JS_IsObject(obj)) return false; + + ScopedJSValue prop(ctx, JS_GetPropertyStr(ctx, obj, prop_name)); + if (!JS_IsUndefined(prop.get()) && !JS_IsNull(prop.get())) { + if (auto* ptr = get_opaque(ctx, prop.get(), class_id)) { + out_val = *ptr; + return true; + } + } + return false; + } + + inline bool try_get_float_property(JSContext* ctx, JSValueConst obj, const char* prop_name, float& out_val) { + if (!JS_IsObject(obj)) return false; + + ScopedJSValue prop(ctx, JS_GetPropertyStr(ctx, obj, prop_name)); + if (!JS_IsUndefined(prop.get()) && JS_IsNumber(prop.get())) { + double temp = 0; + if (JS_ToFloat64(ctx, &temp, prop.get()) == 0) { + out_val = static_cast(temp); + return true; + } + } + return false; + } + + // --- 1. Generic Getters and Setters --- + + template + JSValue js_generic_getter(JSContext* ctx, JSValueConst this_val) { + auto* instance = get_opaque(ctx, this_val, *ClassID); + if (!instance) return JS_EXCEPTION; + + if constexpr (std::is_integral_v) { + return JS_NewInt32(ctx, static_cast(instance->*Member)); + } else if constexpr (std::is_floating_point_v) { + return JS_NewFloat64(ctx, static_cast(instance->*Member)); + } + + return JS_UNDEFINED; + } + + template + JSValue js_generic_setter(JSContext* ctx, JSValueConst this_val, JSValueConst val) { + auto* instance = get_opaque(ctx, this_val, *ClassID); + if (!instance) return JS_EXCEPTION; + + if constexpr (std::is_integral_v) { + int32_t v = 0; + if (JS_ToInt32(ctx, &v, val) != 0) return JS_EXCEPTION; + instance->*Member = static_cast(v); + } else if constexpr (std::is_floating_point_v) { + double v = 0; + if (JS_ToFloat64(ctx, &v, val) != 0) return JS_EXCEPTION; + instance->*Member = static_cast(v); + } + return JS_UNDEFINED; + } + + // --- 2. Exception-Safe Instance Creation --- + + template + JSValue create_js_instance(JSContext* ctx, JSValueConst new_target, JSClassID class_id, Args&&... args) { + ScopedJSValue proto(ctx, JS_GetPropertyStr(ctx, new_target, "prototype")); + if (JS_IsException(proto.get())) return JS_EXCEPTION; + + JSValue obj = JS_NewObjectProtoClass(ctx, proto.get(), class_id); + if (JS_IsException(obj)) return JS_EXCEPTION; + + auto instance = std::make_unique(std::forward(args)...); + JS_SetOpaque(obj, instance.release()); + return obj; + } + + template + JSValue create_class_instance(JSContext* ctx, JSClassID class_id, Args&&... args) { + ScopedJSValue proto(ctx, JS_GetClassProto(ctx, class_id)); + if (JS_IsException(proto.get())) return JS_EXCEPTION; + + JSValue obj = JS_NewObjectProtoClass(ctx, proto.get(), class_id); + if (JS_IsException(obj)) return JS_EXCEPTION; + + auto instance = std::make_unique(std::forward(args)...); + JS_SetOpaque(obj, instance.release()); + return obj; + } + + // Helper forward declaration / overload for JSColor initialization + inline JSValue create_js_color_instance(JSContext* ctx, const ::Color color) { + return create_class_instance(ctx, js_color_class_id, color); + } + + // --- Property & Object Setter Helpers --- + + template + void set_object_property(JSContext* ctx, JSValueConst obj, const char* name, const T& val) { + constexpr int flags = JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE; + + if constexpr (std::is_integral_v) { + JS_DefinePropertyValueStr(ctx, obj, name, JS_NewInt32(ctx, static_cast(val)), flags); + } else if constexpr (std::is_floating_point_v) { + JS_DefinePropertyValueStr(ctx, obj, name, JS_NewFloat64(ctx, static_cast(val)), flags); + } else if constexpr (std::is_convertible_v || std::is_same_v) { + JS_DefinePropertyValueStr(ctx, obj, name, JS_NewString(ctx, std::string(val).c_str()), flags); + } else if constexpr (std::is_same_v) { + JS_DefinePropertyValueStr(ctx, obj, name, create_js_color_instance(ctx, val), flags); + } + } + + template + void set_object_properties(JSContext* ctx, JSValueConst obj, Args&&... entries) { + (set_object_property(ctx, obj, entries.first, entries.second), ...); + } + + // --- Class Registration Helper --- + + struct ClassDefConfig { + std::string_view name; + JSClassID& class_id; + JSClassFinalizer* finalizer = nullptr; + JSCFunction* constructor = nullptr; + std::span proto_funcs{}; + }; + + inline void register_js_class(JSContext* ctx, JSModuleDef* m, const ClassDefConfig& config) { + JSRuntime* const rt = JS_GetRuntime(ctx); + + if (config.class_id == 0) { + JS_NewClassID(rt, &config.class_id); + } + + const JSClassDef class_def{ + .class_name = config.name.data(), + .finalizer = config.finalizer + }; + JS_NewClass(rt, config.class_id, &class_def); + + const ScopedJSValue proto(ctx, JS_NewObject(ctx)); + + if (!config.proto_funcs.empty()) { + JS_SetPropertyFunctionList( + ctx, + proto.get(), + config.proto_funcs.data(), + static_cast(config.proto_funcs.size()) + ); + } + JS_SetClassProto(ctx, config.class_id, proto.get()); + + const JSValue ctor = JS_NewCFunction2(ctx, config.constructor, config.name.data(), 0, JS_CFUNC_constructor, 0); + JS_SetConstructor(ctx, ctor, proto.get()); + + JS_SetModuleExport(ctx, m, config.name.data(), ctor); + } + +} // namespace HostApi::Utils \ No newline at end of file diff --git a/src/core.cpp b/src/core.cpp new file mode 100644 index 0000000..a294076 --- /dev/null +++ b/src/core.cpp @@ -0,0 +1,54 @@ +#include +#include "core.hpp" + +#include "api/hostapi.hpp" +#include "js_engine.hpp" + +namespace VectorJS { + + Core::Core() { + HostApi::make_vectorjs_module(js_engine.get_context()); + } + + void Core::eval_script(const std::string& scriptPath) const { + js_engine.eval_file(scriptPath); + } + + constexpr int DEFAULT_FPS = 60; + constexpr int WIN_HEIGHT = 600; + constexpr int WIN_WIDTH = 800; + + void show_welcome() { + InitWindow(WIN_WIDTH, WIN_HEIGHT, "VectorJS"); + SetTargetFPS(DEFAULT_FPS); + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(RAYWHITE); + DrawText("VectorJS", 40, 40, 80, DARKBLUE); + DrawText("Welcome to the VectorJS!", 40, 140, 20, DARKGRAY); + DrawText(std::format("QuickJS Version: {}", HostApi::QUICKJS_VERSION_STR).c_str(), 40, GetScreenHeight() - 80, 20, LIGHTGRAY); + DrawText(std::format("Raylib Version: {}", HostApi::RAYLIB_VERSION_STR).c_str(), 40, GetScreenHeight() - 60, 20, LIGHTGRAY); + DrawText("Press ESC to exit.", 40, GetScreenHeight() - 40, 20, LIGHTGRAY); + EndDrawing(); + } + CloseWindow(); + } + + void show_bsod(const std::string &errStr) { + if (!IsWindowReady()) { + InitWindow(WIN_WIDTH, WIN_HEIGHT, "VectorJS - Fatal Error"); + SetTargetFPS(DEFAULT_FPS); + } + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLUE); + DrawText(":(", 40, 40, 80, WHITE); + DrawText("Your VectorJS script ran into a problem and crashed.", 40, 140, 20, WHITE); + DrawText(errStr.c_str(), 40, 190, 20, LIGHTGRAY); + DrawText("Press ESC to exit.", 40, GetScreenHeight() - 40, 20, LIGHTGRAY); + EndDrawing(); + } + CloseWindow(); + } + +} \ No newline at end of file diff --git a/src/core.hpp b/src/core.hpp new file mode 100644 index 0000000..b15f034 --- /dev/null +++ b/src/core.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include "js_engine.hpp" + +namespace VectorJS { + + class Core { + public: + // Lifecycle + explicit Core(); + + ~Core() = default; + + // Execution + void eval_script(const std::string& scriptPath) const; + + private: + JSEngine js_engine; + + }; + + void show_welcome(); + void show_bsod(const std::string &errStr); + +} diff --git a/src/js_engine.cpp b/src/js_engine.cpp new file mode 100644 index 0000000..166afa8 --- /dev/null +++ b/src/js_engine.cpp @@ -0,0 +1,104 @@ +#include "js_engine.hpp" +#include +#include +#include +#include + +namespace VectorJS { + void JSEngine::check_and_throw_exception(JSValue value) const { + if (!JS_IsException(value)) { + return; + } + + const JSValue exception_val = JS_GetException(ctx); + std::string error_msg = "JavaScript Exception"; + + const char* msg = JS_ToCString(ctx, exception_val); + if (msg) { + error_msg = msg; + JS_FreeCString(ctx, msg); + } + + // Attempt to extract stack trace + JSValue stack_val = JS_GetPropertyStr(ctx, exception_val, "stack"); + if (!JS_IsUndefined(stack_val)) { + const char* stack_str = JS_ToCString(ctx, stack_val); + if (stack_str) { + error_msg += "\nStack trace:\n"; + error_msg += stack_str; + JS_FreeCString(ctx, stack_str); + } + } + + JS_FreeValue(ctx, stack_val); + JS_FreeValue(ctx, exception_val); + JS_FreeValue(ctx, value); + + throw std::runtime_error(error_msg); + } + + std::string JSEngine::read_file_contents(const std::string& filepath) { + std::ifstream file(filepath, std::ios::in | std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open file: " + filepath); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + JSEngine::JSEngine() { + rt = JS_NewRuntime(); + if (!rt) throw std::runtime_error("Failed to create JSRuntime"); + + ctx = JS_NewContext(rt); + if (!ctx) { + JS_FreeRuntime(rt); + throw std::runtime_error("Failed to create JSContext"); + } + } + + JSEngine::~JSEngine() { + if (ctx) JS_FreeContext(ctx); + if (rt) JS_FreeRuntime(rt); + } + + JSEngine::JSEngine(JSEngine&& other) noexcept : rt(other.rt), ctx(other.ctx) { + other.rt = nullptr; + other.ctx = nullptr; + } + + JSEngine& JSEngine::operator=(JSEngine&& other) noexcept { + if (this != &other) { + if (ctx) JS_FreeContext(ctx); + if (rt) JS_FreeRuntime(rt); + + rt = other.rt; + ctx = other.ctx; + other.rt = nullptr; + other.ctx = nullptr; + } + return *this; + } + + void JSEngine::eval(std::string_view code, std::string_view filename) const { + JSValue result = JS_Eval(ctx, code.data(), code.length(), filename.data(), JS_EVAL_TYPE_MODULE); + check_and_throw_exception(result); + JS_FreeValue(ctx, result); + } + + JSValue JSEngine::eval_value(std::string_view code, std::string_view filename) const { + JSValue result = JS_Eval(ctx, code.data(), code.length(), filename.data(), JS_EVAL_TYPE_MODULE); + check_and_throw_exception(result); + return result; + } + + void JSEngine::eval_file(const std::string& filepath, int eval_flags) const { + std::string code = read_file_contents(filepath); + JSValue result = JS_Eval(ctx, code.c_str(), code.length(), filepath.c_str(), eval_flags); + + // Unified exception handling with stack trace support + check_and_throw_exception(result); + JS_FreeValue(ctx, result); + } +} \ No newline at end of file diff --git a/src/js_engine.hpp b/src/js_engine.hpp new file mode 100644 index 0000000..b80b566 --- /dev/null +++ b/src/js_engine.hpp @@ -0,0 +1,38 @@ +#pragma once + +extern "C" { +#include +} +#include +#include + +namespace VectorJS { + class JSEngine { + JSRuntime* rt{nullptr}; + JSContext* ctx{nullptr}; + + void check_and_throw_exception(JSValue value) const; + static std::string read_file_contents(const std::string& filepath); + + public: + JSEngine(); + ~JSEngine(); + + // Disable copies + JSEngine(const JSEngine&) = delete; + JSEngine& operator=(const JSEngine&) = delete; + + // Enable moves + JSEngine(JSEngine&& other) noexcept; + JSEngine& operator=(JSEngine&& other) noexcept; + + [[nodiscard]] JSContext* get_context() const { return ctx; } + [[nodiscard]] JSRuntime* get_runtime() const { return rt; } + + void eval(std::string_view code, std::string_view filename = "input.js") const; + + [[nodiscard]] JSValue eval_value(std::string_view code, std::string_view filename = "input.js") const; + + void eval_file(const std::string& filepath, int eval_flags = JS_EVAL_TYPE_MODULE) const; + }; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..25a2a90 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,34 @@ +#include +#include "core.hpp" + +int main(const int argc, char** argv) { + + CLI::App cli{"VectorJS"}; + + // Run (argument) + std::string scriptInput; + cli.add_option("script", scriptInput, "Path to the JS game script"); + + try { + cli.parse(argc, argv); + } catch (const CLI::ParseError &e) { + std::cerr << e.what() << std::endl; + return cli.exit(e); + } + + if (scriptInput.empty()) { + VectorJS::show_welcome(); + return 0; + } + + try { + VectorJS::Core app{}; + app.eval_script(scriptInput); + } catch (const std::exception& e) { + std::cerr << "Fatal Error: " << e.what() << std::endl; + VectorJS::show_bsod(e.what()); + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/src/vectorjs.d.ts b/src/vectorjs.d.ts new file mode 100644 index 0000000..4992900 --- /dev/null +++ b/src/vectorjs.d.ts @@ -0,0 +1,124 @@ +declare module "vectorjs" { + + /** + * Structure representing an RGBA color. + */ + export class Color { + r: number; + g: number; + b: number; + a: number; + constructor(); + constructor(r: number, g: number, b: number, a: number); + } + + /** + * Structure representing a 2D vector. + */ + export class Vector2 { + x: number; + y: number; + constructor(); + constructor(x: number, y: number); + } + + /** + * Structure representing a 2D rectangle. + */ + export class Rectangle { + x: number; + y: number; + width: number; + height: number; + constructor(); + constructor(x: number, y: number, width: number, height: number); + } + + /** + * Built-in color palette options matching Raylib defaults. + */ + export const Palette: { + readonly LIGHTGRAY: Color; + readonly GRAY: Color; + readonly DARKGRAY: Color; + readonly YELLOW: Color; + readonly GOLD: Color; + readonly ORANGE: Color; + readonly PINK: Color; + readonly RED: Color; + readonly MAROON: Color; + readonly GREEN: Color; + readonly LIME: Color; + readonly DARKGREEN: Color; + readonly SKYBLUE: Color; + readonly BLUE: Color; + readonly DARKBLUE: Color; + readonly PURPLE: Color; + readonly VIOLET: Color; + readonly DARKPURPLE: Color; + readonly BEIGE: Color; + readonly BROWN: Color; + readonly DARKBROWN: Color; + readonly WHITE: Color; + readonly BLACK: Color; + readonly BLANK: Color; + readonly MAGENTA: Color; + readonly RAYWHITE: Color; + }; + + /** + * Options passed as a raw JS object to shape drawing methods (like drawRectangle). + */ + export interface DrawOptions { + readonly color?: Color; + readonly rotation?: number; + readonly wireframe?: boolean; + readonly origin?: Vector2; + } + + /** + * Exposes individual primitive 2D structural rendering mechanisms. + */ + export interface Render2DShapes { + drawPixel(position: Vector2, color: Color): void; + drawLine(startPosition: Vector2, endPosition: Vector2, options: DrawOptions): void; + drawRectangle(rect: Rectangle, options: DrawOptions): void; + drawCircle(centre: Vector2, radius: number, options: DrawOptions): void; + drawTriangle(point1: Vector2, point2: Vector2, point3: Vector2, options: DrawOptions): void; + drawEllipse(center: Vector2, radiusH: number, radiusV: number, options: DrawOptions): void; + } + + /** + * Context parameter exposed to the `onDraw` hook loop. + */ + export interface RenderContext { + drawFPS(position: Vector2): void; + readonly shapes: Render2DShapes; + withLayer2D(callback: (layer: RenderContext) => void): void; + } + + /** + * Context parameter exposed to the `onUpdate` hook loop. + */ + export interface UpdateContext { + // Currently instantated as empty object context inside create_update_context_object + } + + /** + * Hook listener declarations to tie context events explicitly to runtime loops. + */ + export interface UserApplication { + onInit?(this: UserApplication): void; + onUpdate?(this: UserApplication, ctx: UpdateContext): void; + onDraw?(this: UserApplication, render: RenderContext): void; + } + + /** + * Core Application wrapper executing the rendering pipeline. + */ + export class Application { + constructor(width: number, height: number, title: string); + run(userApp: UserApplication): void; + } + +} \ No newline at end of file