325 lines
16 KiB
C++
325 lines
16 KiB
C++
#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;
|
|
}
|