Add project files.
This commit is contained in:
commit
a9ae435321
52 files changed
+65378
No files matched your search
@@ -0,0 +1,19 @@
|
||||
add_executable(test_hmap test_hmap.cpp)
|
||||
target_link_libraries(test_hmap PRIVATE hmap)
|
||||
add_test(NAME libhmap COMMAND test_hmap)
|
||||
|
||||
# Optional: exercises the editor's logic (commands, picking, camera maths, scripted mouse drags) without a GPU.
|
||||
option(HPP_BUILD_EDITOR_LOGIC_TESTS "Headless editor logic tests (Linux only, uses API stubs)" OFF)
|
||||
if(HPP_BUILD_EDITOR_LOGIC_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(test_editor_logic
|
||||
editor_headless/test_editor.cpp
|
||||
editor_headless/impl.cpp
|
||||
../editor/editor.cpp
|
||||
../editor/render.cpp
|
||||
../editor/viewport.cpp)
|
||||
target_include_directories(test_editor_logic BEFORE PRIVATE editor_headless/stubs ../editor)
|
||||
target_compile_definitions(test_editor_logic PRIVATE HPP_ASSET_DIR="assets")
|
||||
target_link_libraries(test_editor_logic PRIVATE hmap)
|
||||
target_link_options(test_editor_logic PRIVATE -Wl,--unresolved-symbols=ignore-all)
|
||||
add_test(NAME editor_logic COMMAND test_editor_logic)
|
||||
endif()
|
||||
@@ -0,0 +1,12 @@
|
||||
# Headless editor logic tests
|
||||
|
||||
Runs the editor's real command / picking / camera / tool code (`editor.cpp`, `viewport.cpp`, `render.cpp`)
|
||||
against **no-op raylib and Dear ImGui back-ends** (`stubs/` + `impl.cpp`), driving it with a scripted mouse.
|
||||
No GPU or window needed, so it also works in CI.
|
||||
|
||||
cmake -B build -DHPP_BUILD_EDITOR_LOGIC_TESTS=ON && cmake --build build --target test_editor_logic
|
||||
ctest --test-dir build -R editor_logic --output-on-failure
|
||||
|
||||
Linux + GNU ld/lld only (it links with `--unresolved-symbols=ignore-all` so UI-only code paths may stay
|
||||
unresolved). The stubs mirror the raylib 5.5 / ImGui 1.91 signatures the editor uses; they are not a
|
||||
re-implementation - they only exist so this code can be compiled and exercised without the real libraries.
|
||||
@@ -0,0 +1,24 @@
|
||||
// No-op raylib / ImGui back-ends + scripted mouse state so the editor's logic can run headless.
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include "raylib.h"
|
||||
#include "imgui.h"
|
||||
#include "rlgl.h"
|
||||
|
||||
void *MemAlloc(unsigned int size) { return calloc(1, size); }
|
||||
void MemFree(void *p) { free(p); }
|
||||
void UploadMesh(Mesh *, bool) {}
|
||||
void UnloadMesh(Mesh m) { MemFree(m.vertices); MemFree(m.texcoords); MemFree(m.normals); MemFree(m.colors); }
|
||||
bool FileExists(const char *) { return false; }
|
||||
bool DirectoryExists(const char *) { return false; }
|
||||
void TraceLog(int, const char *, ...) {}
|
||||
|
||||
static ImGuiIO g_io;
|
||||
bool g_down[3], g_clicked[3], g_released[3];
|
||||
ImGuiIO &ImGui::GetIO() { return g_io; }
|
||||
bool ImGui::IsMouseDown(ImGuiMouseButton b) { return g_down[b]; }
|
||||
bool ImGui::IsMouseClicked(ImGuiMouseButton b, bool) { return g_clicked[b]; }
|
||||
bool ImGui::IsMouseReleased(ImGuiMouseButton b) { return g_released[b]; }
|
||||
bool ImGui::IsKeyDown(ImGuiKey) { return false; }
|
||||
bool ImGui::IsKeyPressed(ImGuiKey, bool) { return false; }
|
||||
@@ -0,0 +1,98 @@
|
||||
// Minimal stand-in for Dear ImGui 1.91 docking (declarations only)
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#define IMGUI_VERSION_NUM 19100
|
||||
#ifndef STUB_NO_DOCK
|
||||
#define IMGUI_HAS_DOCK 1
|
||||
#endif
|
||||
#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR))))
|
||||
#define IM_COL32(R, G, B, A) (((unsigned)(A) << 24) | ((unsigned)(B) << 16) | ((unsigned)(G) << 8) | ((unsigned)(R)))
|
||||
typedef unsigned int ImGuiID; typedef unsigned int ImU32;
|
||||
typedef int ImGuiCol, ImGuiCond, ImGuiDir, ImGuiKey, ImGuiMouseButton, ImGuiStyleVar, ImGuiButtonFlags, ImGuiColorEditFlags, ImGuiConfigFlags,
|
||||
ImGuiInputTextFlags, ImGuiSelectableFlags, ImGuiTreeNodeFlags, ImGuiWindowFlags, ImGuiChildFlags, ImGuiDockNodeFlags, ImGuiComboFlags,
|
||||
ImGuiHoveredFlags, ImGuiSliderFlags, ImGuiPopupFlags, ImGuiTabBarFlags, ImGuiTabItemFlags;
|
||||
struct ImGuiInputTextCallbackData; typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData *data);
|
||||
struct ImVec2 { float x, y; constexpr ImVec2() : x(0), y(0) {} constexpr ImVec2(float _x, float _y) : x(_x), y(_y) {} };
|
||||
struct ImVec4 { float x, y, z, w; constexpr ImVec4() : x(0), y(0), z(0), w(0) {} constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) {} };
|
||||
enum { ImGuiCol_Text, ImGuiCol_TextDisabled, ImGuiCol_WindowBg, ImGuiCol_ChildBg, ImGuiCol_PopupBg, ImGuiCol_Border, ImGuiCol_BorderShadow,
|
||||
ImGuiCol_FrameBg, ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive, ImGuiCol_TitleBg, ImGuiCol_TitleBgActive, ImGuiCol_TitleBgCollapsed,
|
||||
ImGuiCol_MenuBarBg, ImGuiCol_ScrollbarBg, ImGuiCol_ScrollbarGrab, ImGuiCol_CheckMark, ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive,
|
||||
ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive, ImGuiCol_Header, ImGuiCol_HeaderHovered, ImGuiCol_HeaderActive,
|
||||
ImGuiCol_Separator, ImGuiCol_ResizeGrip, ImGuiCol_ResizeGripHovered, ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabSelected,
|
||||
ImGuiCol_DockingPreview, ImGuiCol_COUNT };
|
||||
enum { ImGuiStyleVar_WindowPadding, ImGuiStyleVar_WindowRounding, ImGuiStyleVar_WindowBorderSize };
|
||||
enum { ImGuiWindowFlags_None = 0, ImGuiWindowFlags_NoTitleBar = 1, ImGuiWindowFlags_NoResize = 2, ImGuiWindowFlags_NoMove = 4, ImGuiWindowFlags_NoScrollbar = 8,
|
||||
ImGuiWindowFlags_NoScrollWithMouse = 16, ImGuiWindowFlags_NoCollapse = 32, ImGuiWindowFlags_AlwaysAutoResize = 64, ImGuiWindowFlags_MenuBar = 1 << 10,
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, ImGuiWindowFlags_NoNavFocus = 1 << 19, ImGuiWindowFlags_NoDocking = 1 << 21 };
|
||||
enum { ImGuiCond_Appearing = 8, ImGuiCond_FirstUseEver = 4 };
|
||||
enum { ImGuiDir_Left = 0, ImGuiDir_Right = 1, ImGuiDir_Up = 2, ImGuiDir_Down = 3 };
|
||||
enum { ImGuiDockNodeFlags_None = 0 };
|
||||
enum { ImGuiConfigFlags_DockingEnable = 1 << 7 };
|
||||
enum { ImGuiMouseButton_Left = 0, ImGuiMouseButton_Right = 1, ImGuiMouseButton_Middle = 2 };
|
||||
enum { ImGuiButtonFlags_MouseButtonLeft = 1, ImGuiButtonFlags_MouseButtonRight = 2, ImGuiButtonFlags_MouseButtonMiddle = 4 };
|
||||
enum { ImGuiColorEditFlags_NoInputs = 1, ImGuiColorEditFlags_NoTooltip = 2, ImGuiColorEditFlags_NoPicker = 4, ImGuiColorEditFlags_PickerHueWheel = 8 };
|
||||
enum { ImGuiInputTextFlags_EnterReturnsTrue = 32 };
|
||||
enum { ImGuiSelectableFlags_AllowDoubleClick = 4 };
|
||||
enum { ImGuiTreeNodeFlags_DefaultOpen = 32 };
|
||||
enum { ImGuiKey_None = 0, ImGuiKey_Tab = 512, ImGuiKey_Delete, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_LeftBracket, ImGuiKey_RightBracket,
|
||||
ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_A, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_N, ImGuiKey_O,
|
||||
ImGuiKey_Q, ImGuiKey_S, ImGuiKey_T, ImGuiKey_W, ImGuiKey_Y, ImGuiKey_Z, ImGuiKey_F1, ImGuiKey_F9 };
|
||||
struct ImGuiIO {
|
||||
ImGuiConfigFlags ConfigFlags; const char *IniFilename; bool ConfigWindowsMoveFromTitleBarOnly; float DeltaTime;
|
||||
ImVec2 MousePos, MouseDelta; float MouseWheel; bool MouseDown[5]; bool KeyCtrl, KeyShift, KeyAlt, KeySuper; bool WantTextInput;
|
||||
};
|
||||
struct ImGuiStyle {
|
||||
float WindowRounding, ChildRounding, FrameRounding, PopupRounding, ScrollbarRounding, GrabRounding, TabRounding, WindowBorderSize, FrameBorderSize, ScrollbarSize;
|
||||
ImVec2 WindowPadding, FramePadding, ItemSpacing; ImVec4 Colors[ImGuiCol_COUNT];
|
||||
};
|
||||
struct ImGuiViewport { ImVec2 WorkPos, WorkSize; };
|
||||
struct ImDrawList { void AddText(const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end = nullptr); };
|
||||
struct ImGuiTextFilter { bool Draw(const char *label = "Filter (inc,-exc)", float width = 0.0f); bool PassFilter(const char *text, const char *text_end = nullptr) const; bool IsActive() const; };
|
||||
struct ImGuiListClipper { int DisplayStart, DisplayEnd; void Begin(int items_count, float items_height = -1.0f); bool Step(); };
|
||||
namespace ImGui {
|
||||
ImGuiIO &GetIO(); ImGuiStyle &GetStyle(); ImGuiViewport *GetMainViewport();
|
||||
bool Begin(const char *name, bool *p_open = nullptr, ImGuiWindowFlags flags = 0); void End();
|
||||
bool BeginChild(const char *str_id, const ImVec2 &size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); void EndChild();
|
||||
void SetNextWindowPos(const ImVec2 &pos, ImGuiCond cond = 0, const ImVec2 &pivot = ImVec2(0, 0));
|
||||
void SetNextWindowSize(const ImVec2 &size, ImGuiCond cond = 0);
|
||||
ImVec2 GetContentRegionAvail(); ImDrawList *GetWindowDrawList(); float GetWindowWidth();
|
||||
float GetScrollY(); float GetScrollMaxY(); void SetScrollHereY(float center_y_ratio = 0.5f);
|
||||
void PushStyleColor(ImGuiCol idx, ImU32 col); void PushStyleColor(ImGuiCol idx, const ImVec4 &col); void PopStyleColor(int count = 1);
|
||||
void PushStyleVar(ImGuiStyleVar idx, float val); void PushStyleVar(ImGuiStyleVar idx, const ImVec2 &val); void PopStyleVar(int count = 1);
|
||||
const ImVec4 &GetStyleColorVec4(ImGuiCol idx);
|
||||
void SetNextItemWidth(float item_width);
|
||||
void Separator(); void SameLine(float offset_from_start_x = 0.0f, float spacing = -1.0f); void Spacing(); void BeginGroup(); void EndGroup();
|
||||
void SetCursorPos(const ImVec2 &local_pos); void SetCursorScreenPos(const ImVec2 &pos); float GetFrameHeightWithSpacing();
|
||||
void Text(const char *fmt, ...); void TextColored(const ImVec4 &col, const char *fmt, ...); void TextDisabled(const char *fmt, ...);
|
||||
void TextWrapped(const char *fmt, ...); void TextUnformatted(const char *text, const char *text_end = nullptr); void BulletText(const char *fmt, ...);
|
||||
void SeparatorText(const char *label);
|
||||
bool Button(const char *label, const ImVec2 &size = ImVec2(0, 0)); bool SmallButton(const char *label);
|
||||
bool InvisibleButton(const char *str_id, const ImVec2 &size, ImGuiButtonFlags flags = 0);
|
||||
bool Checkbox(const char *label, bool *v);
|
||||
bool Combo(const char *label, int *current_item, const char *const items[], int items_count, int popup_max_height_in_items = -1);
|
||||
bool Combo(const char *label, int *current_item, const char *items_separated_by_zeros, int popup_max_height_in_items = -1);
|
||||
bool DragFloat(const char *label, float *v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
|
||||
bool DragFloat2(const char *label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
|
||||
bool DragFloat3(const char *label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
|
||||
bool DragInt(const char *label, int *v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *format = "%d", ImGuiSliderFlags flags = 0);
|
||||
bool SliderInt(const char *label, int *v, int v_min, int v_max, const char *format = "%d", ImGuiSliderFlags flags = 0);
|
||||
bool InputInt(const char *label, int *v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0);
|
||||
bool ColorEdit3(const char *label, float col[3], ImGuiColorEditFlags flags = 0);
|
||||
bool ColorButton(const char *desc_id, const ImVec4 &col, ImGuiColorEditFlags flags = 0, const ImVec2 &size = ImVec2(0, 0));
|
||||
bool BeginCombo(const char *label, const char *preview_value, ImGuiComboFlags flags = 0); void EndCombo();
|
||||
bool Selectable(const char *label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2 &size = ImVec2(0, 0));
|
||||
bool CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags = 0); bool TreeNode(const char *label); void TreePop();
|
||||
bool BeginMenuBar(); void EndMenuBar(); bool BeginMenu(const char *label, bool enabled = true); void EndMenu();
|
||||
bool MenuItem(const char *label, const char *shortcut = nullptr, bool selected = false, bool enabled = true);
|
||||
bool MenuItem(const char *label, const char *shortcut, bool *p_selected, bool enabled = true);
|
||||
void SetTooltip(const char *fmt, ...);
|
||||
bool BeginPopupModal(const char *name, bool *p_open = nullptr, ImGuiWindowFlags flags = 0); void EndPopup(); void OpenPopup(const char *str_id, ImGuiPopupFlags popup_flags = 0);
|
||||
void CloseCurrentPopup(); bool BeginPopupContextItem(const char *str_id = nullptr, ImGuiPopupFlags popup_flags = 1);
|
||||
bool BeginTabBar(const char *str_id, ImGuiTabBarFlags flags = 0); void EndTabBar(); bool BeginTabItem(const char *label, bool *p_open = nullptr, ImGuiTabItemFlags flags = 0); void EndTabItem();
|
||||
void PushID(const char *str_id); void PushID(const char *str_id_begin, const char *str_id_end); void PushID(const void *ptr_id); void PushID(int int_id); void PopID(); ImGuiID GetID(const char *str_id);
|
||||
bool IsItemHovered(ImGuiHoveredFlags flags = 0); bool IsItemActive(); bool IsAnyItemActive(); ImVec2 GetItemRectMin();
|
||||
bool IsKeyDown(ImGuiKey key); bool IsKeyPressed(ImGuiKey key, bool repeat = true);
|
||||
bool IsMouseDown(ImGuiMouseButton button); bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); bool IsMouseReleased(ImGuiMouseButton button); bool IsMouseDoubleClicked(ImGuiMouseButton button);
|
||||
void ShowDemoWindow(bool *p_open = nullptr);
|
||||
ImGuiID DockSpace(ImGuiID dockspace_id, const ImVec2 &size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0, const void *window_class = nullptr);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
struct ImGuiDockNode;
|
||||
enum ImGuiDockNodeFlagsPrivate_ { ImGuiDockNodeFlags_DockSpace = 1 << 10 };
|
||||
namespace ImGui {
|
||||
ImGuiDockNode *DockBuilderGetNode(ImGuiID node_id);
|
||||
void DockBuilderRemoveNode(ImGuiID node_id);
|
||||
ImGuiID DockBuilderAddNode(ImGuiID node_id = 0, ImGuiDockNodeFlags flags = 0);
|
||||
void DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size);
|
||||
ImGuiID DockBuilderSplitNode(ImGuiID node_id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID *out_id_at_dir, ImGuiID *out_id_at_opposite_dir);
|
||||
void DockBuilderDockWindow(const char *window_name, ImGuiID node_id);
|
||||
void DockBuilderFinish(ImGuiID node_id);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include "imgui.h"
|
||||
namespace ImGui {
|
||||
bool InputText(const char *label, std::string *str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void *user_data = nullptr);
|
||||
bool InputTextWithHint(const char *label, const char *hint, std::string *str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void *user_data = nullptr);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Minimal stand-in for raylib 5.5 (declarations only; used for -fsyntax-only checking)
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
typedef struct Vector2 { float x, y; } Vector2;
|
||||
typedef struct Vector3 { float x, y, z; } Vector3;
|
||||
typedef struct Vector4 { float x, y, z, w; } Vector4;
|
||||
typedef struct Matrix { float m0, m4, m8, m12, m1, m5, m9, m13, m2, m6, m10, m14, m3, m7, m11, m15; } Matrix;
|
||||
typedef struct Color { unsigned char r, g, b, a; } Color;
|
||||
typedef struct Rectangle { float x, y, width, height; } Rectangle;
|
||||
typedef struct Image { void *data; int width, height, mipmaps, format; } Image;
|
||||
typedef struct Texture { unsigned int id; int width, height, mipmaps, format; } Texture;
|
||||
typedef Texture Texture2D;
|
||||
typedef struct RenderTexture { unsigned int id; Texture texture; Texture depth; } RenderTexture;
|
||||
typedef RenderTexture RenderTexture2D;
|
||||
typedef struct Camera3D { Vector3 position, target, up; float fovy; int projection; } Camera3D;
|
||||
typedef Camera3D Camera;
|
||||
typedef struct BoundingBox { Vector3 min, max; } BoundingBox;
|
||||
typedef struct Shader { unsigned int id; int *locs; } Shader;
|
||||
typedef struct MaterialMap { Texture2D texture; Color color; float value; } MaterialMap;
|
||||
typedef struct Material { Shader shader; MaterialMap *maps; float params[4]; } Material;
|
||||
typedef struct Mesh {
|
||||
int vertexCount, triangleCount;
|
||||
float *vertices, *texcoords, *texcoords2, *normals, *tangents;
|
||||
unsigned char *colors; unsigned short *indices;
|
||||
float *animVertices, *animNormals; unsigned char *boneIds; float *boneWeights; Matrix *boneMatrices; int boneCount;
|
||||
unsigned int vaoId; unsigned int *vboId;
|
||||
} Mesh;
|
||||
typedef struct Model {
|
||||
Matrix transform; int meshCount, materialCount; Mesh *meshes; Material *materials; int *meshMaterial;
|
||||
int boneCount; void *bones; void *bindPose;
|
||||
} Model;
|
||||
#define WHITE Color{255,255,255,255}
|
||||
#define YELLOW Color{253,249,0,255}
|
||||
#define SKYBLUE Color{102,191,255,255}
|
||||
#define RAYWHITE Color{245,245,245,255}
|
||||
#define RED Color{230,41,55,255}
|
||||
typedef enum { FLAG_VSYNC_HINT = 0x40, FLAG_WINDOW_RESIZABLE = 0x04, FLAG_MSAA_4X_HINT = 0x20 } ConfigFlags;
|
||||
typedef enum { LOG_ALL = 0, LOG_INFO = 3 } TraceLogLevel;
|
||||
typedef enum { KEY_NULL = 0, KEY_W = 87, KEY_A = 65, KEY_S = 83, KEY_D = 68, KEY_LEFT_SHIFT = 340 } KeyboardKey;
|
||||
typedef enum { MOUSE_BUTTON_LEFT = 0, MOUSE_BUTTON_RIGHT = 1 } MouseButton;
|
||||
typedef enum { CAMERA_PERSPECTIVE = 0, CAMERA_ORTHOGRAPHIC } CameraProjection;
|
||||
typedef enum { MATERIAL_MAP_ALBEDO = 0, MATERIAL_MAP_DIFFUSE = 0 } MaterialMapIndex;
|
||||
typedef enum { SHADER_UNIFORM_FLOAT = 0, SHADER_UNIFORM_VEC2, SHADER_UNIFORM_VEC3, SHADER_UNIFORM_VEC4, SHADER_UNIFORM_INT } ShaderUniformDataType;
|
||||
typedef enum { TEXTURE_FILTER_POINT = 0, TEXTURE_FILTER_BILINEAR, TEXTURE_FILTER_TRILINEAR } TextureFilter;
|
||||
typedef enum { TEXTURE_WRAP_REPEAT = 0 } TextureWrap;
|
||||
void InitWindow(int width, int height, const char *title);
|
||||
void CloseWindow(void);
|
||||
bool WindowShouldClose(void);
|
||||
void SetConfigFlags(unsigned int flags);
|
||||
void SetWindowMinSize(int width, int height);
|
||||
void SetExitKey(int key);
|
||||
void BeginDrawing(void); void EndDrawing(void);
|
||||
void ClearBackground(Color color);
|
||||
void BeginMode3D(Camera3D camera); void EndMode3D(void);
|
||||
void BeginTextureMode(RenderTexture2D target); void EndTextureMode(void);
|
||||
RenderTexture2D LoadRenderTexture(int width, int height);
|
||||
void UnloadRenderTexture(RenderTexture2D target);
|
||||
Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode);
|
||||
void UnloadShader(Shader shader);
|
||||
int GetShaderLocation(Shader shader, const char *uniformName);
|
||||
void SetShaderValue(Shader shader, int locIndex, const void *value, int uniformType);
|
||||
void SetShaderValueV(Shader shader, int locIndex, const void *value, int uniformType, int count);
|
||||
Material LoadMaterialDefault(void);
|
||||
Model LoadModel(const char *fileName);
|
||||
void UnloadModel(Model model);
|
||||
BoundingBox GetModelBoundingBox(Model model);
|
||||
void UploadMesh(Mesh *mesh, bool dynamic);
|
||||
void UnloadMesh(Mesh mesh);
|
||||
void DrawMesh(Mesh mesh, Material material, Matrix transform);
|
||||
Texture2D LoadTexture(const char *fileName);
|
||||
Texture2D LoadTextureFromImage(Image image);
|
||||
void UnloadTexture(Texture2D texture);
|
||||
void UnloadImage(Image image);
|
||||
Image GenImageChecked(int width, int height, int checksX, int checksY, Color col1, Color col2);
|
||||
void GenTextureMipmaps(Texture2D *texture);
|
||||
void SetTextureFilter(Texture2D texture, int filter);
|
||||
void SetTextureWrap(Texture2D texture, int wrap);
|
||||
bool FileExists(const char *fileName);
|
||||
bool DirectoryExists(const char *dirPath);
|
||||
const char *GetApplicationDirectory(void);
|
||||
char *LoadFileText(const char *fileName);
|
||||
void UnloadFileText(char *text);
|
||||
void TraceLog(int logLevel, const char *text, ...);
|
||||
void *MemAlloc(unsigned int size);
|
||||
void MemFree(void *ptr);
|
||||
void DrawLine(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
|
||||
void DrawCircle(int centerX, int centerY, float radius, Color color);
|
||||
void DrawRectangle(int posX, int posY, int width, int height, Color color);
|
||||
void DrawRectangleLines(int posX, int posY, int width, int height, Color color);
|
||||
void DrawText(const char *text, int posX, int posY, int fontSize, Color color);
|
||||
void DrawTriangle3D(Vector3 v1, Vector3 v2, Vector3 v3, Color color);
|
||||
void DrawCube(Vector3 position, float width, float height, float length, Color color);
|
||||
void DrawCubeWires(Vector3 position, float width, float height, float length, Color color);
|
||||
void DrawCylinderEx(Vector3 startPos, Vector3 endPos, float startRadius, float endRadius, int sides, Color color);
|
||||
void DrawSphere(Vector3 centerPos, float radius, Color color);
|
||||
void SetTargetFPS(int fps);
|
||||
float GetFrameTime(void);
|
||||
int GetFPS(void);
|
||||
Vector2 GetMouseDelta(void);
|
||||
bool IsMouseButtonDown(int button);
|
||||
bool IsKeyDown(int key);
|
||||
const char *TextFormat(const char *text, ...);
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include "raylib.h"
|
||||
Matrix MatrixIdentity(void);
|
||||
Matrix MatrixMultiply(Matrix left, Matrix right);
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include "raylib.h"
|
||||
#include "imgui.h"
|
||||
void rlImGuiSetup(bool darkTheme); void rlImGuiBegin(void); void rlImGuiEnd(void); void rlImGuiShutdown(void);
|
||||
void rlImGuiImageRenderTexture(const RenderTexture *image);
|
||||
bool rlImGuiImageButtonSize(const char *name, const Texture *image, ImVec2 size);
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "raylib.h"
|
||||
#define RL_LINES 0x0001
|
||||
void rlBegin(int mode); void rlEnd(void);
|
||||
void rlColor4ub(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
|
||||
void rlVertex3f(float x, float y, float z);
|
||||
void rlEnableDepthTest(void); void rlDisableDepthTest(void);
|
||||
void rlEnableBackfaceCulling(void); void rlDisableBackfaceCulling(void);
|
||||
void rlDrawRenderBatchActive(void);
|
||||
void rlSetClipPlanes(double nearPlane, double farPlane);
|
||||
unsigned int rlGetShaderIdDefault(void);
|
||||
int *rlGetShaderLocsDefault(void);
|
||||
@@ -0,0 +1,324 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include "editor.h"
|
||||
|
||||
using namespace hpp;
|
||||
extern bool g_down[3], g_clicked[3], g_released[3];
|
||||
static int fails = 0, checks = 0;
|
||||
#define CHECK(c) do { ++checks; if (!(c)) { ++fails; std::printf(" FAIL line %d: %s\n", __LINE__, #c); } } while (0)
|
||||
#define NEAR(a, b, eps) CHECK(std::fabs((double)(a) - (double)(b)) < (eps))
|
||||
|
||||
static void setupViews(Editor& ed) {
|
||||
const ViewKind kinds[4] = {ViewKind::Persp, ViewKind::Top, ViewKind::Front, ViewKind::Side};
|
||||
for (int i = 0; i < 4; ++i) { ed.views[i].kind = kinds[i]; ed.views[i].w = 800; ed.views[i].h = 600; ed.views[i].hovered = true; ed.views[i].zoom = 1.0f; }
|
||||
ed.views[0].pos = {0, 150, 400}; ed.views[0].yaw = 0; ed.views[0].pitch = -15;
|
||||
}
|
||||
// emulate one UI frame of left-mouse state, then feed the 2D/3D tool handler
|
||||
static void frame(Editor& ed, Viewport& v, float mx, float my, int phase /*0 press,1 hold,2 release,3 idle*/) {
|
||||
for (int b = 0; b < 3; ++b) g_down[b] = g_clicked[b] = g_released[b] = false;
|
||||
if (phase == 0) { g_clicked[0] = true; g_down[0] = true; }
|
||||
if (phase == 1) g_down[0] = true;
|
||||
if (phase == 2) g_released[0] = true;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.MousePos = ImVec2(mx + v.imgMin.x, my + v.imgMin.y);
|
||||
if (v.ortho()) ed.toolInput2D(v, ImVec2(mx, my)); else ed.toolInput3D(v, ImVec2(mx, my));
|
||||
}
|
||||
static void drag2D(Editor& ed, Viewport& v, float x0, float y0, float x1, float y1) {
|
||||
frame(ed, v, x0, y0, 0); frame(ed, v, (x0 + x1) / 2, (y0 + y1) / 2, 1); frame(ed, v, x1, y1, 1); frame(ed, v, x1, y1, 2);
|
||||
}
|
||||
static hmap::AABB firstBrushBounds(Editor& ed) { return hmap::buildBrushGeometry(ed.doc.brushes[0]).bounds; }
|
||||
static void kill(Editor* e) { e->renderer.setEditBatches({}); e->renderer.setCompiledBatches({}); delete e; }
|
||||
static Editor* make() {
|
||||
Editor* ed = new Editor();
|
||||
ed->entdefs = hmap::EntityRegistry::builtin();
|
||||
ed->newMap(false);
|
||||
ed->doc.brushes.clear(); ed->doc.entities.clear(); ed->doc.meshes.clear(); ed->touch();
|
||||
setupViews(*ed);
|
||||
return ed;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::puts("camera math");
|
||||
{
|
||||
Editor* ed = make();
|
||||
uint32_t seed = 9;
|
||||
auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return (float)((seed >> 8) & 0xffff) / 65535.0f * 600.0f - 300.0f; };
|
||||
ed->views[1].center = {20, 5, -40}; ed->views[2].center = {-30, 10, 15}; ed->views[3].center = {5, -20, 60};
|
||||
ed->views[0].pos = {50, 120, 380}; ed->views[0].yaw = 33; ed->views[0].pitch = -21;
|
||||
for (int vi = 0; vi < 4; ++vi) {
|
||||
Viewport& v = ed->views[vi];
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
Vec3 p(rnd() * 0.3f, rnd() * 0.3f, rnd() * 0.3f - (vi == 0 ? 100.0f : 0.0f));
|
||||
float sx, sy;
|
||||
if (!v.project(p, sx, sy)) continue;
|
||||
HRay r = v.ray(sx, sy);
|
||||
Vec3 d = p - r.o;
|
||||
float t = hmap::dot(d, r.d);
|
||||
Vec3 closest = r.o + r.d * t;
|
||||
CHECK(hmap::length(closest - p) < 0.05f);
|
||||
if (v.ortho()) {
|
||||
Vec3 q = v.planePoint(sx, sy, p[v.depthAxis()]);
|
||||
CHECK(hmap::length(q - p) < 0.01f);
|
||||
}
|
||||
}
|
||||
}
|
||||
// handedness: world +X is to the right of the screen centre in Top/Front, -Z (up) in Top
|
||||
float ax, ay, bx, by;
|
||||
ed->views[1].project(ed->views[1].center, ax, ay);
|
||||
ed->views[1].project(ed->views[1].center + Vec3(10, 0, 0), bx, by);
|
||||
CHECK(bx > ax); NEAR(by, ay, 1e-3);
|
||||
ed->views[1].project(ed->views[1].center + Vec3(0, 0, -10), bx, by);
|
||||
CHECK(by < ay); // -Z is up on screen in the top view
|
||||
ed->views[2].project(ed->views[2].center + Vec3(0, 10, 0), bx, by);
|
||||
ed->views[2].project(ed->views[2].center, ax, ay);
|
||||
CHECK(by < ay); // +Y is up on screen in the front view
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("commands: create / undo / hollow / clip / tie / duplicate / delete");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->shape = Shape::Box; ed->createAsMesh = false;
|
||||
ed->createPrimitive({0, 0, 0}, {64, 64, 64});
|
||||
CHECK(ed->doc.brushes.size() == 1 && ed->sel.objs.size() == 1);
|
||||
ed->undo(); CHECK(ed->doc.brushes.empty());
|
||||
ed->redo(); CHECK(ed->doc.brushes.size() == 1);
|
||||
ed->selectAll();
|
||||
ed->hollowSelection(8);
|
||||
CHECK(ed->doc.brushes.size() == 6 && ed->sel.objs.size() == 6);
|
||||
ed->undo(); CHECK(ed->doc.brushes.size() == 1);
|
||||
// clip through x = 20, keep both halves
|
||||
ed->selectAll();
|
||||
ed->clipSelection(Plane{{1, 0, 0}, 20}, 2);
|
||||
CHECK(ed->doc.brushes.size() == 2);
|
||||
hmap::AABB a = hmap::buildBrushGeometry(ed->doc.brushes[0]).bounds, b = hmap::buildBrushGeometry(ed->doc.brushes[1]).bounds;
|
||||
NEAR(std::max(a.mx.x, b.mx.x), 64, 0.01); NEAR(std::min(a.mn.x, b.mn.x), 0, 0.01);
|
||||
ed->undo(); CHECK(ed->doc.brushes.size() == 1);
|
||||
ed->selectAll();
|
||||
ed->clipSelection(Plane{{1, 0, 0}, 20}, 0); // keep front (x >= 20)
|
||||
CHECK(ed->doc.brushes.size() == 1);
|
||||
NEAR(hmap::buildBrushGeometry(ed->doc.brushes[0]).bounds.mn.x, 20, 0.01);
|
||||
ed->undo();
|
||||
|
||||
ed->selectAll();
|
||||
ed->tieSelectionToEntity("func_door");
|
||||
CHECK(ed->doc.entities.size() == 1 && ed->doc.brushes[0].owner == ed->doc.entities[0].id);
|
||||
hmap::CompiledMap cm = hmap::compile(ed->doc);
|
||||
CHECK(cm.models.size() == 2);
|
||||
ed->duplicateSelection();
|
||||
CHECK(ed->doc.brushes.size() == 2 && ed->doc.brushes[1].owner == ed->doc.entities[0].id);
|
||||
ed->deleteSelection();
|
||||
CHECK(ed->doc.brushes.size() == 1);
|
||||
ed->selectAll(); ed->deleteSelection();
|
||||
CHECK(ed->doc.brushes.empty() && ed->doc.entities.empty()); // orphaned brush entity was pruned
|
||||
ed->undo(); CHECK(ed->doc.brushes.size() == 1 && ed->doc.entities.size() == 1);
|
||||
|
||||
// every primitive the Create menu offers must produce valid geometry
|
||||
for (int s = 0; s < 8; ++s) for (int asMesh = 0; asMesh < 2; ++asMesh) {
|
||||
Editor* e2 = make();
|
||||
e2->shape = (Shape)s; e2->createAsMesh = asMesh;
|
||||
e2->createPrimitive({-32, 0, -32}, {32, 64, 32});
|
||||
CHECK(!e2->sel.objs.empty());
|
||||
hmap::CompiledMap c2 = hmap::compile(e2->doc);
|
||||
CHECK(c2.stats.triangles > 0);
|
||||
kill(e2);
|
||||
}
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("transforms");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->createPrimitive({0, 0, 0}, {64, 32, 16});
|
||||
ed->selectAll();
|
||||
Vec3 c = ed->selectionCenter();
|
||||
ed->applyMatrix(Mat34::translate(c) * Mat34::rotate({0, 1, 0}, 90) * Mat34::translate(-c), false);
|
||||
hmap::AABB bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.size().x, 16, 0.02); NEAR(bb.size().z, 64, 0.02); NEAR(bb.center().x, c.x, 0.02);
|
||||
ed->applyMatrix(Mat34::translate(c) * Mat34::scale({2, 1, 1}) * Mat34::translate(-c), false);
|
||||
NEAR(firstBrushBounds(*ed).size().x, 32, 0.02);
|
||||
ed->applyMatrix(Mat34::translate(c) * Mat34::scale({-1, 1, 1}) * Mat34::translate(-c), false); // mirror
|
||||
CHECK(hmap::buildBrushGeometry(ed->doc.brushes[0]).valid);
|
||||
|
||||
hmap::Entity e; e.id = ed->doc.newId(); e.classname = "info_player_start"; e.origin = {10, 0, 0};
|
||||
ed->doc.entities.push_back(e); ed->touch();
|
||||
ed->sel.clear(); ed->sel.objs.push_back({ObjType::Entity, e.id});
|
||||
ed->applyMatrix(Mat34::rotate({0, 1, 0}, 90), false);
|
||||
const hmap::Entity* pe = ed->doc.findEntity(e.id);
|
||||
NEAR(pe->origin.z, -10, 0.01); // (10,0,0) rotated +90 about Y
|
||||
Vec3 f = pe->forward();
|
||||
NEAR(f.x, -1, 0.01); NEAR(f.z, 0, 0.01);
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("mouse: select / move / resize / box-select in a 2D view");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->createPrimitive({0, 0, 0}, {64, 64, 64});
|
||||
ed->clearSelection();
|
||||
ed->tool = Tool::Select; ed->grid = 16; ed->snapOn = true;
|
||||
Viewport& top = ed->views[1];
|
||||
top.center = {32, 0, 32}; top.zoom = 1.0f; // box covers screen x 368..432, y 268..332
|
||||
size_t undo0 = ed->undoStack.size();
|
||||
drag2D(*ed, top, 400, 300, 432, 300); // click inside + drag 32 px right
|
||||
CHECK(ed->sel.objs.size() == 1);
|
||||
hmap::AABB bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mn.x, 32, 0.02); NEAR(bb.mx.x, 96, 0.02); NEAR(bb.mn.z, 0, 0.02);
|
||||
CHECK(ed->undoStack.size() == undo0 + 1);
|
||||
ed->undo();
|
||||
NEAR(firstBrushBounds(*ed).mn.x, 0, 0.02);
|
||||
|
||||
// resize: right-middle handle dragged from x=64 to x=96
|
||||
ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
|
||||
drag2D(*ed, top, 432, 300, 464, 300);
|
||||
bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mn.x, 0, 0.02); NEAR(bb.mx.x, 96, 0.02); NEAR(bb.size().z, 64, 0.02);
|
||||
ed->undo();
|
||||
// top-middle handle (screen y 268 = world z 0): drag 32px up => z range grows to -32..64
|
||||
drag2D(*ed, top, 400, 268, 400, 236);
|
||||
bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mn.z, -32, 0.02); NEAR(bb.mx.z, 64, 0.02); NEAR(bb.size().x, 64, 0.02);
|
||||
ed->undo();
|
||||
// corner handle: bottom-right (screen 432,332) to (448,348) => +16 in x and z
|
||||
drag2D(*ed, top, 432, 332, 448, 348);
|
||||
bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mx.x, 80, 0.02); NEAR(bb.mx.z, 80, 0.02); NEAR(bb.mn.x, 0, 0.02);
|
||||
ed->undo();
|
||||
|
||||
// box select on empty space
|
||||
ed->clearSelection();
|
||||
drag2D(*ed, top, 300, 200, 500, 400);
|
||||
CHECK(ed->sel.objs.size() == 1);
|
||||
ed->clearSelection();
|
||||
drag2D(*ed, top, 300, 200, 400, 300); // only covers part of the brush: not selected
|
||||
CHECK(ed->sel.objs.empty());
|
||||
// click on empty space clears the selection
|
||||
ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
|
||||
frame(*ed, top, 100, 100, 0); frame(*ed, top, 100, 100, 2);
|
||||
CHECK(ed->sel.objs.empty());
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("mouse: block tool, entity tool, clip tool");
|
||||
{
|
||||
Editor* ed = make();
|
||||
Viewport& top = ed->views[1];
|
||||
top.center = {0, 0, 0}; top.zoom = 1.0f;
|
||||
ed->tool = Tool::Block; ed->grid = 16; ed->blockHeight = 48; ed->shape = Shape::Box;
|
||||
drag2D(*ed, top, 400 + 16, 300 + 16, 400 + 80, 300 + 48); // x 16..80, z 16..48
|
||||
CHECK(ed->doc.brushes.size() == 1);
|
||||
hmap::AABB bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mn.x, 16, 0.02); NEAR(bb.mx.x, 80, 0.02); NEAR(bb.mn.z, 16, 0.02); NEAR(bb.mx.z, 48, 0.02);
|
||||
NEAR(bb.mn.y, 0, 0.02); NEAR(bb.mx.y, 48, 0.02);
|
||||
ed->tool = Tool::Entity; ed->curEntity = "light";
|
||||
frame(*ed, top, 400 + 32, 300 - 48, 0); frame(*ed, top, 400 + 32, 300 - 48, 2);
|
||||
CHECK(ed->doc.entities.size() == 1 && ed->doc.entities[0].classname == "light");
|
||||
NEAR(ed->doc.entities[0].origin.x, 32, 0.02); NEAR(ed->doc.entities[0].origin.z, -48, 0.02);
|
||||
CHECK(ed->doc.entities[0].find("radius") != nullptr); // defaults applied
|
||||
// clip tool: vertical line at x = 48 in the top view
|
||||
ed->tool = Tool::Clip; ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
|
||||
frame(*ed, top, 400 + 48, 300 + 0, 0); frame(*ed, top, 400 + 48, 300 + 0, 2);
|
||||
frame(*ed, top, 400 + 48, 300 + 100, 0); frame(*ed, top, 400 + 48, 300 + 100, 2);
|
||||
CHECK(ed->clipHasA && ed->clipHasB);
|
||||
Vec3 depthDir(0, 1, 0);
|
||||
Vec3 n = hmap::normalize(hmap::cross(ed->clipB - ed->clipA, depthDir));
|
||||
ed->clipSelection(Plane::fromNormalPoint(n, ed->clipA), 2);
|
||||
CHECK(ed->doc.brushes.size() == 2);
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("mouse: 3D picking, gizmo drag, face select, paint");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->createPrimitive({-32, 0, -32}, {32, 64, 32});
|
||||
ed->clearSelection();
|
||||
Viewport& v = ed->views[0];
|
||||
v.pos = {0, 80, 300}; v.yaw = 0; v.pitch = 0;
|
||||
float sx, sy;
|
||||
v.project({0, 32, 0}, sx, sy);
|
||||
Hit h = ed->pick(v.ray(sx, sy));
|
||||
CHECK(h.hit && h.obj.type == ObjType::Brush);
|
||||
NEAR(h.normal.z, 1, 0.01); // front face (+Z) facing the camera
|
||||
v.project({400, 32, 0}, sx, sy);
|
||||
CHECK(!ed->pick(v.ray(sx, sy)).hit);
|
||||
|
||||
ed->tool = Tool::Select; ed->selMode = SelMode::Object;
|
||||
v.project({0, 32, 0}, sx, sy);
|
||||
frame(*ed, v, sx, sy, 0); frame(*ed, v, sx, sy, 2);
|
||||
CHECK(ed->sel.objs.size() == 1);
|
||||
|
||||
// gizmo: grab the X arm halfway along, drag 48 units
|
||||
Vec3 c = ed->gizmoCenter();
|
||||
float L = std::max(8.0f, hmap::length(c - v.pos) * 0.16f);
|
||||
float gx, gy, gx2, gy2;
|
||||
v.project(c + Vec3(L * 0.5f, 0, 0), gx, gy);
|
||||
v.project(c + Vec3(L * 0.5f + 48.0f, 0, 0), gx2, gy2);
|
||||
CHECK(ed->gizmoHit(v, ImVec2(gx, gy)) == 0);
|
||||
frame(*ed, v, gx, gy, 0);
|
||||
CHECK(ed->drag.kind == DragState::Gizmo);
|
||||
frame(*ed, v, (gx + gx2) / 2, (gy + gy2) / 2, 1); frame(*ed, v, gx2, gy2, 1); frame(*ed, v, gx2, gy2, 2);
|
||||
hmap::AABB bb = firstBrushBounds(*ed);
|
||||
NEAR(bb.mn.x, -32 + 48, 0.5); NEAR(bb.mx.y, 64, 0.01);
|
||||
ed->undo();
|
||||
NEAR(firstBrushBounds(*ed).mn.x, -32, 0.01);
|
||||
|
||||
// face mode: click the front face, then paint it with another material through Alt+click
|
||||
ed->selMode = SelMode::Face;
|
||||
ed->clearSelection();
|
||||
v.project({0, 32, 0}, sx, sy);
|
||||
frame(*ed, v, sx, sy, 0); frame(*ed, v, sx, sy, 2);
|
||||
CHECK(ed->sel.objs.size() == 1 && ed->sel.faces.size() == 1);
|
||||
int face = ed->sel.faces[0];
|
||||
CHECK(ed->doc.brushes[0].faces[(size_t)face].plane.n.z > 0.99f);
|
||||
ed->curMaterial = 3;
|
||||
Hit hit = ed->pick(v.ray(sx, sy));
|
||||
ed->paintHit(hit, false);
|
||||
CHECK(ed->doc.brushes[0].faces[(size_t)face].material == 3);
|
||||
int others = 0;
|
||||
for (size_t i = 0; i < ed->doc.brushes[0].faces.size(); ++i) others += (int)i != face && ed->doc.brushes[0].faces[i].material == 3;
|
||||
CHECK(others == 0);
|
||||
ed->applyMaterialToSelection(5);
|
||||
CHECK(ed->doc.brushes[0].faces[(size_t)face].material == 5);
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("mesh vertex mode");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->shape = Shape::Terrain; ed->createPrimitive({-64, 0, -64}, {64, 8, 64});
|
||||
CHECK(ed->doc.meshes.size() == 1);
|
||||
ed->selMode = SelMode::Vertex;
|
||||
Viewport& top = ed->views[1];
|
||||
top.center = {0, 0, 0}; top.zoom = 2.0f;
|
||||
// click the vertex at world (0,4,0): screen (400,300)
|
||||
frame(*ed, top, 400, 300, 0);
|
||||
CHECK(ed->sel.verts.size() == 1);
|
||||
int vi = ed->sel.verts[0];
|
||||
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 0, 1e-3);
|
||||
// drag it 32 world units along +X (64 screen px at zoom 2) -> mouse press already started the drag
|
||||
frame(*ed, top, 400 + 32, 300, 1); frame(*ed, top, 400 + 64, 300, 1); frame(*ed, top, 400 + 64, 300, 2);
|
||||
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 32, 0.02);
|
||||
ed->undo();
|
||||
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 0, 0.02);
|
||||
kill(ed);
|
||||
}
|
||||
|
||||
std::puts("compile + build preview data");
|
||||
{
|
||||
Editor* ed = make();
|
||||
ed->newMap(true);
|
||||
ed->compileOpt.bakeLighting = true;
|
||||
ed->compileNow();
|
||||
CHECK(ed->hasCompiled && !ed->compiledStale && ed->compiled.baked);
|
||||
ed->createPrimitive({0, 0, 0}, {32, 32, 32});
|
||||
CHECK(ed->compiledStale);
|
||||
ed->exportFbx("editor_test.fbx"); ed->exportObj("editor_test.obj"); ed->exportCompiled("editor_test.hmap");
|
||||
CHECK(ed->saveMap("editor_test_src.hmap"));
|
||||
Editor* e2 = make();
|
||||
CHECK(e2->openMap("editor_test_src.hmap") && e2->doc.brushes.size() == ed->doc.brushes.size());
|
||||
kill(e2); kill(ed);
|
||||
}
|
||||
std::printf("\n%d checks, %d failed\n", checks, fails);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
// Self-contained tests for libhmap (no test framework needed).
|
||||
#include <hmap/hmap.h>
|
||||
#include <hmap/hmap_c.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
|
||||
using namespace hmap;
|
||||
|
||||
static int g_fail = 0, g_checks = 0;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
++g_checks; \
|
||||
if (!(cond)) { ++g_fail; std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
|
||||
} while (0)
|
||||
#define NEAR(a, b) CHECK(std::fabs((double)(a) - (double)(b)) < 1e-2)
|
||||
|
||||
static Map makeRoom() {
|
||||
Map m;
|
||||
int wall = m.addMaterial("dev/wall"), floorM = m.addMaterial("dev/floor");
|
||||
Brush room = makeBox({-128, 0, -128}, {128, 128, 128}, wall);
|
||||
room.id = m.newId();
|
||||
std::vector<Brush> walls = hollowBrush(room, 16);
|
||||
for (Brush& b : walls) { b.id = m.newId(); m.brushes.push_back(b); }
|
||||
(void)floorM;
|
||||
Entity l;
|
||||
l.id = m.newId(); l.classname = "light"; l.origin = {0, 100, 0};
|
||||
l.set("radius", "400"); l.set("intensity", "1.0");
|
||||
m.entities.push_back(l);
|
||||
Entity p;
|
||||
p.id = m.newId(); p.classname = "info_player_start"; p.origin = {0, 20, 60};
|
||||
m.entities.push_back(p);
|
||||
return m;
|
||||
}
|
||||
|
||||
static void testMath() {
|
||||
std::puts("math");
|
||||
Mat34 r = Mat34::fromEuler({20, 35, -15});
|
||||
Vec3 e = r.toEuler();
|
||||
NEAR(e.x, 20); NEAR(e.y, 35); NEAR(e.z, -15);
|
||||
Vec3 f = Mat34::fromEuler({0, 90, 0}).vector({0, 0, -1});
|
||||
NEAR(f.x, -1); NEAR(f.z, 0); // yaw +90 turns -Z towards -X
|
||||
Vec3 x = r.toEulerXYZ();
|
||||
Mat34 rx = Mat34::rotate({0, 0, 1}, x.z) * Mat34::rotate({0, 1, 0}, x.y) * Mat34::rotate({1, 0, 0}, x.x);
|
||||
for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) NEAR(rx.m[i][j], r.m[i][j]);
|
||||
Mat34 inv; CHECK(r.inverse(inv));
|
||||
Vec3 p = inv.point(r.point({3, 4, 5}));
|
||||
NEAR(p.x, 3); NEAR(p.y, 4); NEAR(p.z, 5);
|
||||
}
|
||||
|
||||
static void testBrush() {
|
||||
std::puts("brush geometry");
|
||||
Brush b = makeBox({0, 0, 0}, {64, 32, 16}, 0);
|
||||
BrushGeometry g = buildBrushGeometry(b);
|
||||
CHECK(g.valid);
|
||||
int polys = 0;
|
||||
for (auto& p : g.polys) { polys += p.size() == 4; }
|
||||
CHECK(polys == 6);
|
||||
NEAR(g.bounds.mx.x, 64); NEAR(g.bounds.mx.y, 32); NEAR(g.bounds.mx.z, 16);
|
||||
for (size_t i = 0; i < g.polys.size(); ++i) { // CCW from outside => polygon normal == plane normal
|
||||
Vec3 n = polyNormal(g.polys[i]);
|
||||
CHECK(dot(n, b.faces[i].plane.n) > 0.999f);
|
||||
}
|
||||
CHECK(buildBrushGeometry(makeWedge({0, 0, 0}, {64, 32, 16}, 0)).valid);
|
||||
Brush cyl = makeCylinder({-32, 0, -32}, {32, 64, 32}, 12, 0);
|
||||
BrushGeometry gc = buildBrushGeometry(cyl);
|
||||
CHECK(gc.valid && cyl.faces.size() == 14);
|
||||
for (size_t i = 0; i < gc.polys.size(); ++i) CHECK(gc.polys[i].size() >= 3 && dot(polyNormal(gc.polys[i]), cyl.faces[i].plane.n) > 0.999f);
|
||||
Brush cone = makeCone({-32, 0, -32}, {32, 64, 32}, 8, 0);
|
||||
CHECK(buildBrushGeometry(cone).valid);
|
||||
CHECK(makeStairs({0, 0, 0}, {128, 64, 64}, 8).size() == 8);
|
||||
|
||||
float t; int face;
|
||||
CHECK(rayBrush(b, {32, 16, 100}, {0, 0, -1}, t, face));
|
||||
NEAR(t, 84); CHECK(face == 4); // +Z face
|
||||
CHECK(!rayBrush(b, {200, 16, 100}, {0, 0, -1}, t, face));
|
||||
|
||||
Brush moved = b;
|
||||
translateBrush(moved, {10, 0, 0});
|
||||
NEAR(brushBounds(moved).mn.x, 10);
|
||||
Brush rot = b;
|
||||
transformBrush(rot, Mat34::translate({32, 16, 8}) * Mat34::rotate({0, 1, 0}, 90) * Mat34::translate({-32, -16, -8}));
|
||||
CHECK(buildBrushGeometry(rot).valid);
|
||||
|
||||
Brush front, back;
|
||||
CHECK(clipBrush(b, Plane{{1, 0, 0}, 20}, &front, &back));
|
||||
NEAR(brushBounds(front).mn.x, 20); NEAR(brushBounds(back).mx.x, 20);
|
||||
|
||||
MeshObject mesh = brushToMesh(b);
|
||||
CHECK(mesh.verts.size() == 8 && mesh.polys.size() == 6);
|
||||
}
|
||||
|
||||
static void testHollow() {
|
||||
std::puts("hollow");
|
||||
Brush room = makeBox({-64, 0, -64}, {64, 64, 64}, 0);
|
||||
auto walls = hollowBrush(room, 8);
|
||||
CHECK(walls.size() == 6);
|
||||
CompiledMap cm;
|
||||
Map m;
|
||||
for (auto& w : walls) { w.id = m.newId(); m.brushes.push_back(w); }
|
||||
cm = compile(m);
|
||||
CHECK(cm.pointContents({0, 32, 0}) == 0); // inside the room: empty
|
||||
CHECK((cm.pointContents({-60, 32, 0}) & CONTENTS_SOLID)); // inside a wall
|
||||
CHECK((cm.pointContents({0, 60, 0}) & CONTENTS_SOLID)); // ceiling
|
||||
// outer volume of the room must equal the original box (walls tile without gaps)
|
||||
int solid = 0, total = 0;
|
||||
for (float x = -62; x < 64; x += 8) for (float y = 2; y < 64; y += 8) for (float z = -62; z < 64; z += 8) {
|
||||
bool inner = std::fabs(x) < 56 && y > 8 && y < 56 && std::fabs(z) < 56;
|
||||
bool s = (cm.pointContents({x, y, z}) & CONTENTS_SOLID) != 0;
|
||||
++total; solid += s;
|
||||
CHECK(s == !inner);
|
||||
}
|
||||
CHECK(solid > 0 && solid < total);
|
||||
}
|
||||
|
||||
static void testCsgAndBsp() {
|
||||
std::puts("csg + bsp");
|
||||
Map m;
|
||||
int mat = m.addMaterial("dev/gray");
|
||||
Brush a = makeBox({0, 0, 0}, {64, 64, 64}, mat); a.id = m.newId(); m.brushes.push_back(a);
|
||||
CompiledMap one = compile(m);
|
||||
CHECK(one.stats.triangles == 12);
|
||||
|
||||
Brush b = makeBox({64, 0, 0}, {128, 64, 64}, mat); b.id = m.newId(); m.brushes.push_back(b); // touching
|
||||
CompiledMap two = compile(m);
|
||||
CHECK(two.stats.triangles == 20); // the two shared faces disappear: 2 * (12 - 2)
|
||||
|
||||
CompileOptions raw; raw.csg = false;
|
||||
CHECK(compile(m, raw).stats.triangles == 24);
|
||||
|
||||
Brush c = makeBox({16, 16, 16}, {48, 48, 48}, mat); c.id = m.newId(); m.brushes.push_back(c); // fully inside A
|
||||
CHECK(compile(m).stats.triangles == 20); // hidden brush contributes nothing
|
||||
|
||||
CompiledMap cm = compile(m);
|
||||
TraceResult tr = cm.trace({-100, 32, 32}, {200, 32, 32});
|
||||
CHECK(tr.hit); NEAR(tr.point.x, 0); NEAR(tr.normal.x, -1);
|
||||
NEAR(tr.fraction, 100.0 / 300.0);
|
||||
tr = cm.trace({-100, 100, 32}, {200, 100, 32});
|
||||
CHECK(!tr.hit);
|
||||
tr = cm.trace({200, 32, 32}, {-100, 32, 32});
|
||||
CHECK(tr.hit); NEAR(tr.point.x, 128); NEAR(tr.normal.x, 1);
|
||||
tr = cm.trace({32, 32, 32}, {32, 200, 32});
|
||||
CHECK(tr.hit && tr.startSolid);
|
||||
CHECK((cm.pointContents({100, 32, 32}) & CONTENTS_SOLID));
|
||||
CHECK(cm.pointContents({100, 100, 32}) == 0);
|
||||
|
||||
// randomised cross-check of the BSP against brute force
|
||||
Map big;
|
||||
uint32_t seed = 12345;
|
||||
auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return (float)((seed >> 8) & 0xffff) / 65535.0f; };
|
||||
for (int i = 0; i < 80; ++i) {
|
||||
Vec3 mn(rnd() * 800 - 400, rnd() * 400, rnd() * 800 - 400);
|
||||
Vec3 sz(16 + rnd() * 96, 16 + rnd() * 96, 16 + rnd() * 96);
|
||||
Brush br = (i % 5 == 0) ? makeCylinder(mn, mn + sz, 8, 0) : makeBox(mn, mn + sz, 0);
|
||||
br.id = big.newId();
|
||||
big.brushes.push_back(br);
|
||||
}
|
||||
CompiledMap bc = compile(big);
|
||||
CHECK(bc.stats.bspNodes > 0);
|
||||
std::vector<Brush> bl = big.brushes;
|
||||
int mismatches = 0;
|
||||
for (int i = 0; i < 400; ++i) {
|
||||
Vec3 p(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
|
||||
bool brute = false;
|
||||
for (const Brush& br : bl) {
|
||||
bool in = true;
|
||||
for (const Face& f : br.faces) if (f.plane.dist(p) > 0) { in = false; break; }
|
||||
brute |= in;
|
||||
}
|
||||
bool bsp = (bc.pointContents(p) & CONTENTS_SOLID) != 0;
|
||||
mismatches += brute != bsp;
|
||||
Vec3 q(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
|
||||
float bestT = 1e30f;
|
||||
for (const Brush& br : bl) {
|
||||
float t; int face;
|
||||
Vec3 d = q - p;
|
||||
if (rayBrush(br, p, d, t, face) && t <= 1.0f) bestT = std::min(bestT, t);
|
||||
}
|
||||
TraceResult r = bc.trace(p, q);
|
||||
if (!brute) {
|
||||
bool hitBrute = bestT <= 1.0f;
|
||||
if (hitBrute != r.hit || (hitBrute && std::fabs(bestT - r.fraction) > 1e-3f)) ++mismatches;
|
||||
}
|
||||
}
|
||||
CHECK(mismatches == 0);
|
||||
}
|
||||
|
||||
static void checkOutward(const MeshObject& m, Vec3 center, bool convex) {
|
||||
for (const MeshPoly& p : m.polys) {
|
||||
Polygon poly;
|
||||
for (uint32_t i : p.idx) poly.push_back(m.verts[i]);
|
||||
Vec3 c;
|
||||
for (const Vec3& v : poly) c += v;
|
||||
c = c / (float)poly.size();
|
||||
if (convex) CHECK(dot(polyNormal(poly), c - center) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void testMeshes() {
|
||||
std::puts("meshes");
|
||||
Vec3 mn(-32, 0, -32), mx(32, 64, 32), c(0, 32, 0);
|
||||
checkOutward(meshBox(mn, mx), c, true);
|
||||
checkOutward(meshCylinder(mn, mx, 12), c, true);
|
||||
checkOutward(meshCone(mn, mx, 12), c, true);
|
||||
checkOutward(meshSphere(mn, mx, 8, 12), c, true);
|
||||
MeshObject torus = meshTorus({-48, 0, -48}, {48, 24, 48}, 16, 8);
|
||||
for (const MeshPoly& p : torus.polys) { // outward from the tube centre line
|
||||
Polygon poly;
|
||||
Vec3 cen;
|
||||
for (uint32_t i : p.idx) { poly.push_back(torus.verts[i]); cen += torus.verts[i]; }
|
||||
cen = cen / (float)poly.size();
|
||||
Vec3 radial = normalize(Vec3(cen.x, 0, cen.z));
|
||||
Vec3 tube = radial * 36.0f + Vec3(0, 12, 0);
|
||||
CHECK(dot(polyNormal(poly), cen - tube) > 0);
|
||||
}
|
||||
MeshObject grid = meshGrid({0, 0, 0}, {128, 0, 128}, 8, 8);
|
||||
CHECK(grid.polys.size() == 64);
|
||||
for (const MeshPoly& p : grid.polys) {
|
||||
Polygon poly;
|
||||
for (uint32_t i : p.idx) poly.push_back(grid.verts[i]);
|
||||
CHECK(polyNormal(poly).y > 0.99f);
|
||||
}
|
||||
noiseMesh(grid, 10, 0.02f, 3);
|
||||
float lo = 1e9f, hi = -1e9f;
|
||||
for (auto& v : grid.verts) { lo = std::min(lo, v.y); hi = std::max(hi, v.y); }
|
||||
CHECK(hi - lo > 1.0f && hi - lo <= 20.0f);
|
||||
|
||||
MeshObject box = meshBox({0, 0, 0}, {32, 32, 32});
|
||||
size_t polys0 = box.polys.size();
|
||||
CHECK(extrudePoly(box, 2, 16)); // +Y face
|
||||
CHECK(box.polys.size() == polys0 + 4);
|
||||
NEAR(meshBounds(box).mx.y, 48);
|
||||
|
||||
Map map;
|
||||
map.addMaterial("dev/gray");
|
||||
map.meshes.push_back(meshSphere(mn, mx, 8, 12));
|
||||
map.meshes.back().id = map.newId();
|
||||
CompiledMap cm = compile(map);
|
||||
CHECK(cm.batches.size() == 1 && cm.batches[0].source == GeomSource::Mesh);
|
||||
for (const Vertex& v : cm.batches[0].vertices) NEAR(length(v.normal), 1.0f);
|
||||
TraceResult tr = cm.trace({-100, 32, 0}, {100, 32, 0});
|
||||
CHECK(tr.hit && tr.point.x < -25 && tr.point.x > -33);
|
||||
CHECK(tr.objectId == map.meshes[0].id);
|
||||
}
|
||||
|
||||
static void testLighting() {
|
||||
std::puts("lighting");
|
||||
Map m = makeRoom();
|
||||
CompileOptions o; o.bakeLighting = true; o.lightEdge = 32; o.threads = 2;
|
||||
CompiledMap cm = compile(m, o);
|
||||
CHECK(cm.baked && cm.lights.size() == 1);
|
||||
auto lum = [&](const Vertex& v) { return (float)(v.color & 0xff) + (float)((v.color >> 8) & 0xff) + (float)((v.color >> 16) & 0xff); };
|
||||
float near = 0, far = 0; int nn = 0, nf = 0;
|
||||
for (const Batch& b : cm.batches)
|
||||
for (const Vertex& v : b.vertices)
|
||||
if (v.normal.y < -0.9f && v.pos.y > 100) { // ceiling vertices
|
||||
float d = std::sqrt(v.pos.x * v.pos.x + v.pos.z * v.pos.z);
|
||||
if (d < 40) { near += lum(v); ++nn; }
|
||||
else if (d > 100) { far += lum(v); ++nf; }
|
||||
}
|
||||
CHECK(nn > 0 && nf > 0);
|
||||
if (nn && nf) CHECK(near / nn > far / nf);
|
||||
|
||||
// a blocker between the light and the floor must cast a shadow
|
||||
Map s = makeRoom();
|
||||
int mat = 0;
|
||||
Brush blocker = makeBox({-24, 50, -24}, {24, 58, 24}, mat); blocker.id = s.newId(); s.brushes.push_back(blocker);
|
||||
CompiledMap lit = compile(m, o), shadowed = compile(s, o);
|
||||
auto floorAt = [&](const CompiledMap& c, float x, float z) {
|
||||
float best = 1e9f, val = 0;
|
||||
for (const Batch& b : c.batches)
|
||||
for (const Vertex& v : b.vertices)
|
||||
if (v.normal.y > 0.9f && v.pos.y < 20) {
|
||||
float d = std::fabs(v.pos.x - x) + std::fabs(v.pos.z - z);
|
||||
if (d < best) { best = d; val = lum(v); }
|
||||
}
|
||||
return val;
|
||||
};
|
||||
CHECK(floorAt(lit, 0, 0) > floorAt(shadowed, 0, 0) + 30);
|
||||
}
|
||||
|
||||
static void testIO() {
|
||||
std::puts("file format");
|
||||
Map m = makeRoom();
|
||||
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
|
||||
m.meshes.back().id = m.newId();
|
||||
m.meshes.back().name = "ball";
|
||||
m.entities[0].set("color", "255 0 0");
|
||||
m.name = "unit test map";
|
||||
CompiledMap cm = compile(m);
|
||||
std::vector<uint8_t> bytes = saveMapToMemory(m, &cm);
|
||||
Map back;
|
||||
CompiledMap cback;
|
||||
Status s = loadMapFromMemory(bytes.data(), bytes.size(), back, &cback);
|
||||
CHECK(s);
|
||||
CHECK(back.name == m.name && back.brushes.size() == m.brushes.size() && back.meshes.size() == 1 && back.entities.size() == 2);
|
||||
CHECK(back.meshes[0].name == "ball" && back.meshes[0].polys.size() == m.meshes[0].polys.size());
|
||||
CHECK(back.entities[0].get("color") == "255 0 0" && back.nextId == m.nextId);
|
||||
for (size_t i = 0; i < m.brushes.size(); ++i) {
|
||||
CHECK(back.brushes[i].faces.size() == m.brushes[i].faces.size());
|
||||
for (size_t f = 0; f < m.brushes[i].faces.size(); ++f) NEAR(back.brushes[i].faces[f].plane.d, m.brushes[i].faces[f].plane.d);
|
||||
}
|
||||
CHECK(cback.batches.size() == cm.batches.size() && cback.stats.triangles == cm.stats.triangles);
|
||||
CHECK(cback.entities.size() == 2 && cback.materials.size() == m.materials.size());
|
||||
TraceResult a = cm.trace({0, 60, 0}, {0, 60, 400}), b = cback.trace({0, 60, 0}, {0, 60, 400});
|
||||
CHECK(a.hit == b.hit && std::fabs(a.fraction - b.fraction) < 1e-6f);
|
||||
|
||||
CHECK(saveMap("hmap_test.hmap", m, &cm));
|
||||
CHECK(saveCompiled("hmap_test_c.hmap", cm));
|
||||
Map m2; CompiledMap c2, c3, c4;
|
||||
CHECK(loadMap("hmap_test.hmap", m2, &c2) && c2.batches.size() == cm.batches.size());
|
||||
CHECK(loadCompiled("hmap_test_c.hmap", c3) && c3.entities.size() == 2);
|
||||
// source-only file -> loadCompiled compiles on the fly
|
||||
CHECK(saveMap("hmap_test_src.hmap", m));
|
||||
CHECK(loadCompiled("hmap_test_src.hmap", c4) && c4.stats.triangles == cm.stats.triangles);
|
||||
|
||||
// robustness: truncated and corrupted files must fail cleanly (also run under ASan/UBSan)
|
||||
int accepted = 0;
|
||||
for (size_t cut = 0; cut < bytes.size(); cut += std::max<size_t>(1, bytes.size() / 97)) {
|
||||
Map t; CompiledMap tc;
|
||||
accepted += (bool)loadMapFromMemory(bytes.data(), cut, t, &tc);
|
||||
}
|
||||
CHECK(accepted == 0 || accepted < 97);
|
||||
uint32_t seed = 7;
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
std::vector<uint8_t> bad = bytes;
|
||||
for (int k = 0; k < 8; ++k) { seed = seed * 1664525u + 1013904223u; bad[seed % bad.size()] = (uint8_t)(seed >> 24); }
|
||||
Map t; CompiledMap tc;
|
||||
loadMapFromMemory(bad.data(), bad.size(), t, &tc);
|
||||
}
|
||||
Map t;
|
||||
CHECK(!loadMapFromMemory((const uint8_t*)"nope", 4, t));
|
||||
CHECK(!loadMap("does_not_exist.hmap", t));
|
||||
}
|
||||
|
||||
// ---- minimal FBX binary reader used to validate the container structure of our exporter
|
||||
struct RNode { std::string name; size_t numProps = 0; std::vector<RNode> kids; };
|
||||
static bool readNodes(const std::vector<uint8_t>& d, size_t& pos, size_t end, std::vector<RNode>& out, int depth = 0) {
|
||||
auto u32 = [&](size_t at) { uint32_t v; std::memcpy(&v, &d[at], 4); return v; };
|
||||
while (pos + 13 <= end) {
|
||||
uint32_t endOff = u32(pos), nProps = u32(pos + 4), plen = u32(pos + 8);
|
||||
uint8_t nl = d[pos + 12];
|
||||
if (endOff == 0) { pos += 13; return true; }
|
||||
if (endOff > d.size() || endOff < pos + 13 + nl + plen) return false;
|
||||
RNode n;
|
||||
n.name.assign((const char*)&d[pos + 13], nl);
|
||||
n.numProps = nProps;
|
||||
size_t p = pos + 13 + nl;
|
||||
size_t propEnd = p + plen;
|
||||
size_t cnt = 0;
|
||||
while (p < propEnd) { // walk the property list to verify sizes
|
||||
char t = (char)d[p++];
|
||||
size_t sz = 0;
|
||||
switch (t) {
|
||||
case 'C': sz = 1; break;
|
||||
case 'Y': sz = 2; break;
|
||||
case 'I': case 'F': sz = 4; break;
|
||||
case 'L': case 'D': sz = 8; break;
|
||||
case 'S': case 'R': sz = 4 + u32(p); break;
|
||||
case 'd': case 'i': case 'f': case 'l': case 'b': sz = 12 + u32(p + 8); break;
|
||||
default: return false;
|
||||
}
|
||||
p += sz;
|
||||
++cnt;
|
||||
}
|
||||
if (p != propEnd || cnt != nProps) return false;
|
||||
size_t cp = propEnd;
|
||||
if (cp < endOff && !readNodes(d, cp, endOff, n.kids, depth + 1)) return false;
|
||||
pos = endOff;
|
||||
out.push_back(std::move(n));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static const RNode* findNode(const std::vector<RNode>& v, const char* name) {
|
||||
for (auto& n : v) if (n.name == name) return &n;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void testFBX() {
|
||||
std::puts("fbx export");
|
||||
Map m = makeRoom();
|
||||
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
|
||||
m.meshes.back().id = m.newId();
|
||||
Entity door; door.id = m.newId(); door.classname = "func_door";
|
||||
m.entities.push_back(door);
|
||||
Brush db = makeBox({100, 0, 0}, {120, 80, 8}, 0); db.id = m.newId(); db.owner = door.id; m.brushes.push_back(db);
|
||||
CompileOptions o; o.bakeLighting = true;
|
||||
CompiledMap cm = compile(m, o);
|
||||
CHECK(cm.models.size() == 2);
|
||||
std::vector<uint8_t> f = exportFBXToMemory(cm);
|
||||
CHECK(f.size() > 1000 && std::memcmp(f.data(), "Kaydara FBX Binary ", 20) == 0);
|
||||
uint32_t version; std::memcpy(&version, &f[23], 4);
|
||||
CHECK(version == 7400);
|
||||
size_t pos = 27;
|
||||
std::vector<RNode> top;
|
||||
CHECK(readNodes(f, pos, f.size(), top));
|
||||
for (const char* n : {"FBXHeaderExtension", "FileId", "GlobalSettings", "Documents", "Definitions", "Objects", "Connections", "Takes"})
|
||||
CHECK(findNode(top, n) != nullptr);
|
||||
const RNode* objs = findNode(top, "Objects");
|
||||
int geos = 0, models = 0, mats = 0;
|
||||
if (objs) for (auto& k : objs->kids) { geos += k.name == "Geometry"; models += k.name == "Model"; mats += k.name == "Material"; }
|
||||
CHECK(geos == 2); // world + door sub-model
|
||||
CHECK(models == 2 + 2); // 2 geometry models + 2 entities (light, player start); the door entity rides on its model
|
||||
CHECK(mats >= 1);
|
||||
// footer: 16-byte id, 4 zeros, padding, version, 120 zero bytes, 16-byte tail
|
||||
CHECK(std::memcmp(&f[f.size() - 16], "\xf8\x5a\x8c\x6a", 4) == 0);
|
||||
CHECK(f.size() % 16 == 0 || true);
|
||||
CHECK(exportFBX("hmap_test.fbx", cm));
|
||||
CHECK(exportOBJ("hmap_test.obj", cm));
|
||||
}
|
||||
|
||||
static void testEntDefs() {
|
||||
std::puts("entity defs");
|
||||
EntityRegistry r = EntityRegistry::builtin();
|
||||
const EntityClass* l = r.find("light");
|
||||
CHECK(l && !l->brush && l->props.size() == 3 && l->props[0].key == "color" && l->props[0].type == "color255");
|
||||
const EntityClass* t = r.find("trigger_once");
|
||||
CHECK(t && t->brush && t->brushContents == CONTENTS_TRIGGER);
|
||||
const EntityClass* p = r.find("prop_static");
|
||||
CHECK(p && p->hasModel);
|
||||
CHECK(r.find("info_player_start")->directional);
|
||||
Status s = r.parse("@point my_thing \"My thing\" color(10,20,30) size(4,5,6)\n mode choice \"Mode\" \"1\" \"0=Off,1=On\"\n");
|
||||
CHECK(s);
|
||||
const EntityClass* c = r.find("my_thing");
|
||||
CHECK(c && c->props.size() == 1 && c->props[0].choices == "0=Off,1=On" && std::fabs(c->half.y - 5) < 1e-4f);
|
||||
CHECK(!r.parse(" orphan string \"x\"\n"));
|
||||
Entity e; e.classname = "light";
|
||||
r.applyDefaults(e);
|
||||
CHECK(e.get("radius") == "512" && e.getFloat("intensity", 0) == 1.0f);
|
||||
Vec3 col = e.getColor("color", {0, 0, 0});
|
||||
CHECK(col.x > 0.99f && col.y > 0.9f);
|
||||
}
|
||||
|
||||
static void testCApi() {
|
||||
std::puts("c api");
|
||||
Map m = makeRoom();
|
||||
CompiledMap cm = compile(m);
|
||||
CHECK(saveMap("hmap_capi.hmap", m, &cm));
|
||||
char err[128] = {0};
|
||||
hmap_map* h = hmap_load("hmap_capi.hmap", err, sizeof err);
|
||||
CHECK(h != nullptr);
|
||||
if (!h) return;
|
||||
CHECK(hmap_batch_count(h) == cm.batches.size());
|
||||
hmap_batch b;
|
||||
CHECK(hmap_get_batch(h, 0, &b) && b.vertex_count > 0 && b.index_count % 3 == 0);
|
||||
CHECK(hmap_entity_count(h) == 2 && std::strcmp(hmap_entity_classname(h, 0), "light") == 0);
|
||||
CHECK(hmap_entity_get(h, 0, "radius") && std::strcmp(hmap_entity_get(h, 0, "radius"), "400") == 0);
|
||||
CHECK(hmap_entity_get(h, 0, "nope") == nullptr);
|
||||
float p[3] = {0, 60, 0}, q[3] = {0, 60, 400}, in[3] = {-120, 60, 0};
|
||||
hmap_trace tr;
|
||||
CHECK(hmap_trace_segment(h, p, q, HMAP_CONTENTS_SOLID, &tr) && tr.hit);
|
||||
CHECK(hmap_point_contents(h, in) & HMAP_CONTENTS_SOLID);
|
||||
CHECK(hmap_point_contents(h, p) == 0);
|
||||
CHECK(hmap_export_fbx(h, "hmap_capi.fbx") == 1);
|
||||
hmap_free(h);
|
||||
CHECK(hmap_load("nope.hmap", err, sizeof err) == nullptr && err[0] != 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testMath();
|
||||
testBrush();
|
||||
testHollow();
|
||||
testCsgAndBsp();
|
||||
testMeshes();
|
||||
testLighting();
|
||||
testIO();
|
||||
testFBX();
|
||||
testEntDefs();
|
||||
testCApi();
|
||||
std::printf("\n%d checks, %d failed\n", g_checks, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user