Files
2026-10-25 03:29:04 -03:00

1003 lines
45 KiB
C++

#include <algorithm>
#include <cmath>
#include <cstdio>
#include "editor.h"
#include "rlImGui.h"
namespace hpp {
using hmap::dot;
using hmap::kDeg2Rad;
using hmap::length;
using hmap::normalize;
static const Color kSelColor = {255, 128, 48, 255};
static const Color kVertColor = {120, 220, 255, 255};
static const Color kGridMinor = {52, 57, 70, 255};
static const Color kGridMajor = {78, 86, 108, 255};
static Color toColor(const Vec3& c, float a = 1.0f) {
auto q = [](float v) { return (unsigned char)(std::max(0.0f, std::min(1.0f, v)) * 255.0f); };
return Color{q(c.x), q(c.y), q(c.z), q(a)};
}
// ============================================================ camera math
Vec3 Viewport::forward() const {
switch (kind) {
case ViewKind::Top: return {0, -1, 0};
case ViewKind::Front: return {0, 0, -1};
case ViewKind::Side: return {-1, 0, 0};
default: {
float y = yaw * kDeg2Rad, p = pitch * kDeg2Rad;
return {-std::sin(y) * std::cos(p), std::sin(p), -std::cos(y) * std::cos(p)};
}
}
}
Vec3 Viewport::right() const {
switch (kind) {
case ViewKind::Top: return {1, 0, 0};
case ViewKind::Front: return {1, 0, 0};
case ViewKind::Side: return {0, 0, -1};
default: {
float y = yaw * kDeg2Rad;
return {std::cos(y), 0, -std::sin(y)};
}
}
}
Vec3 Viewport::up() const {
switch (kind) {
case ViewKind::Top: return {0, 0, -1};
case ViewKind::Front: return {0, 1, 0};
case ViewKind::Side: return {0, 1, 0};
default: return hmap::cross(right(), forward());
}
}
int Viewport::depthAxis() const {
switch (kind) {
case ViewKind::Top: return 1;
case ViewKind::Side: return 0;
default: return 2;
}
}
Camera3D Viewport::camera() const {
Camera3D c = {};
Vec3 f = forward();
if (ortho()) {
c.position = toRl(center - f * 50000.0f);
c.target = toRl(center);
c.up = toRl(up());
c.projection = CAMERA_ORTHOGRAPHIC;
c.fovy = (float)h / zoom; // visible height in world units
} else {
c.position = toRl(pos);
c.target = toRl(pos + f);
c.up = {0, 1, 0};
c.projection = CAMERA_PERSPECTIVE;
c.fovy = fov;
}
return c;
}
HRay Viewport::ray(float sx, float sy) const {
Vec3 f = forward(), r = right(), u = up();
if (ortho()) {
Vec3 o = center + r * ((sx - (float)w * 0.5f) / zoom) + u * (-(sy - (float)h * 0.5f) / zoom);
return {o - f * 40000.0f, f};
}
float th = std::tan(fov * 0.5f * kDeg2Rad), aspect = (float)w / (float)h;
float nx = 2.0f * sx / (float)w - 1.0f, ny = 1.0f - 2.0f * sy / (float)h;
return {pos, normalize(f + r * (nx * th * aspect) + u * (ny * th))};
}
bool Viewport::project(const Vec3& p, float& sx, float& sy) const {
Vec3 f = forward(), r = right(), u = up();
if (ortho()) {
Vec3 d = p - center;
sx = (float)w * 0.5f + dot(d, r) * zoom;
sy = (float)h * 0.5f - dot(d, u) * zoom;
return true;
}
Vec3 d = p - pos;
float z = dot(d, f);
if (z < 0.5f) return false;
float th = std::tan(fov * 0.5f * kDeg2Rad), aspect = (float)w / (float)h;
sx = (dot(d, r) / (z * th * aspect) * 0.5f + 0.5f) * (float)w;
sy = (0.5f - dot(d, u) / (z * th) * 0.5f) * (float)h;
return true;
}
Vec3 Viewport::planePoint(float sx, float sy, float depth) const {
Vec3 p = center + right() * ((sx - (float)w * 0.5f) / zoom) + up() * (-(sy - (float)h * 0.5f) / zoom);
p[depthAxis()] = depth;
return p;
}
// ============================================================ helpers
static float segDist(float px, float py, float ax, float ay, float bx, float by) {
float dx = bx - ax, dy = by - ay, l2 = dx * dx + dy * dy;
float t = l2 > 1e-6f ? std::max(0.0f, std::min(1.0f, ((px - ax) * dx + (py - ay) * dy) / l2)) : 0.0f;
float cx = ax + dx * t - px, cy = ay + dy * t - py;
return std::sqrt(cx * cx + cy * cy);
}
// parameter along the line (O + s*D) closest to the mouse ray
static bool axisParam(const Vec3& O, const Vec3& D, const HRay& r, float& s) {
Vec3 w0 = O - r.o;
float a = dot(D, D), b = dot(D, r.d), c = dot(r.d, r.d), d = dot(D, w0), e = dot(r.d, w0);
float den = a * c - b * b;
if (std::fabs(den) < 1e-6f) return false;
s = (b * e - c * d) / den;
return true;
}
static Vec3 dominantAxis(const Vec3& n) {
float ax = std::fabs(n.x), ay = std::fabs(n.y), az = std::fabs(n.z);
if (ay >= ax && ay >= az) return {0, n.y >= 0 ? 1.0f : -1.0f, 0};
if (ax >= az) return {n.x >= 0 ? 1.0f : -1.0f, 0, 0};
return {0, 0, n.z >= 0 ? 1.0f : -1.0f};
}
static int nearestVertex(Viewport& v, const hmap::MeshObject& m, const ImVec2& mouse, float maxPx) {
int best = -1;
float bd = maxPx * maxPx;
for (size_t i = 0; i < m.verts.size(); ++i) {
float sx, sy;
if (!v.project(m.verts[i], sx, sy)) continue;
float d = (sx - mouse.x) * (sx - mouse.x) + (sy - mouse.y) * (sy - mouse.y);
if (d < bd) { bd = d; best = (int)i; }
}
return best;
}
bool Editor::gizmoVisible() const {
if (tool != Tool::Select) return false;
return selMode == SelMode::Vertex ? !sel.verts.empty() : !sel.objs.empty();
}
Vec3 Editor::gizmoCenter() {
if (selMode == SelMode::Vertex) {
ObjRef p = sel.primary();
if (p.type == ObjType::Mesh)
if (const hmap::MeshObject* m = doc.findMesh(p.id)) {
Vec3 c;
int n = 0;
for (int i : sel.verts)
if (i >= 0 && (size_t)i < m->verts.size()) { c += m->verts[(size_t)i]; ++n; }
if (n) return c / (float)n;
}
}
return selectionCenter();
}
static float gizmoLength(const Viewport& v, const Vec3& c) { return std::max(8.0f, length(c - v.pos) * 0.16f); }
int Editor::gizmoHit(Viewport& v, const ImVec2& m) {
Vec3 c = gizmoCenter();
float L = gizmoLength(v, c), best = 12.0f;
int bi = -1;
for (int a = 0; a < 3; ++a) {
Vec3 e = c;
e[a] += L;
float cx, cy, ex, ey;
if (!v.project(c, cx, cy) || !v.project(e, ex, ey)) continue;
float d = segDist(m.x, m.y, cx, cy, ex, ey);
if (d < best) { best = d; bi = a; }
}
return bi;
}
bool Editor::selectionRect(Viewport& v, float& u0, float& u1, float& v0, float& v1) {
if (sel.empty()) return false;
AABB bb;
if (selMode == SelMode::Vertex) {
ObjRef p = sel.primary();
const hmap::MeshObject* m = p.type == ObjType::Mesh ? doc.findMesh(p.id) : nullptr;
if (!m || sel.verts.empty()) return false;
for (int i : sel.verts)
if (i >= 0 && (size_t)i < m->verts.size()) bb.add(m->verts[(size_t)i]);
} else {
bb = selectionBounds();
}
if (bb.empty()) return false;
Vec3 r = v.right(), u = v.up();
float a = dot(bb.mn, r), b = dot(bb.mx, r), c = dot(bb.mn, u), d = dot(bb.mx, u);
u0 = std::min(a, b); u1 = std::max(a, b);
v0 = std::min(c, d); v1 = std::max(c, d);
return true;
}
void Editor::focusSelection() {
AABB bb = selectionBounds();
if (bb.empty()) {
for (size_t i = 0; i < doc.brushes.size(); ++i) bb.add(scene().brushGeo[i].bounds);
for (const AABB& m : scene().meshBounds) bb.add(m);
for (const hmap::Entity& e : doc.entities) bb.add(e.origin);
}
if (bb.empty()) bb.add(Vec3(-128, 0, -128)), bb.add(Vec3(128, 128, 128));
Vec3 c = bb.center(), s = bb.size();
float ext = std::max(48.0f, std::max(s.x, std::max(s.y, s.z)));
for (Viewport& v : views) {
if (v.ortho()) {
v.center = c;
float inPlane = std::max(std::fabs(dot(s, v.right())), std::fabs(dot(s, v.up())));
v.zoom = std::max(0.02f, std::min(8.0f, (float)std::min(v.w, v.h) / (std::max(48.0f, inPlane) * 1.5f)));
} else {
v.pos = c - v.forward() * (ext * 1.7f + 64.0f);
}
}
}
// ============================================================ window
void Editor::drawViewport(Viewport& v) {
if (!v.open) return;
switch (v.kind) {
case ViewKind::Persp: firstPlace(0.17f, 0.04f, 0.30f, 0.45f); break;
case ViewKind::Top: firstPlace(0.48f, 0.04f, 0.30f, 0.45f); break;
case ViewKind::Front: firstPlace(0.17f, 0.50f, 0.30f, 0.30f); break;
case ViewKind::Side: firstPlace(0.48f, 0.50f, 0.30f, 0.30f); break;
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
bool visible = ImGui::Begin(v.title, &v.open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
if (visible) {
ImVec2 avail = ImGui::GetContentRegionAvail();
int w = std::max(32, (int)avail.x), h = std::max(32, (int)avail.y);
if (v.rt.id == 0 || v.w != w || v.h != h) {
if (v.rt.id != 0) UnloadRenderTexture(v.rt);
v.rt = LoadRenderTexture(w, h);
v.w = w;
v.h = h;
}
rlImGuiImageRenderTexture(&v.rt);
v.imgMin = ImGui::GetItemRectMin();
ImGui::SetCursorScreenPos(v.imgMin);
ImGui::InvisibleButton("##viewport", ImVec2((float)w, (float)h),
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);
v.hovered = ImGui::IsItemHovered();
bool active = ImGui::IsItemActive();
viewInput(v, active);
renderView(v);
static const char* shadeNames[] = {"Wireframe", "Flat", "Lit", "Baked"};
char info[160];
if (v.ortho()) std::snprintf(info, sizeof info, "%s grid %g zoom %.0f%%", v.title, grid, v.zoom * 100.0f);
else std::snprintf(info, sizeof info, "%s %s speed %.0f", v.title, shadeNames[(int)shading], v.flySpeed);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddText(ImVec2(v.imgMin.x + 9, v.imgMin.y + 7), IM_COL32(0, 0, 0, 160), info);
dl->AddText(ImVec2(v.imgMin.x + 8, v.imgMin.y + 6), IM_COL32(225, 230, 242, 235), info);
if (!v.ortho() && shading == Shading::Baked && (!hasCompiled || compiledStale))
dl->AddText(ImVec2(v.imgMin.x + 8, v.imgMin.y + 24), IM_COL32(255, 190, 90, 255),
hasCompiled ? "Baked preview is out of date - press F9 to rebuild" : "Nothing baked yet - press F9 to build");
} else {
v.hovered = false;
}
ImGui::End();
}
// ============================================================ input
void Editor::viewInput(Viewport& v, bool active) {
ImGuiIO& io = ImGui::GetIO();
ImVec2 m(io.MousePos.x - v.imgMin.x, io.MousePos.y - v.imgMin.y);
float dt = io.DeltaTime;
bool rmb = io.MouseDown[ImGuiMouseButton_Right], mmb = io.MouseDown[ImGuiMouseButton_Middle];
if (v.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) v.flying = true;
if (!rmb) v.flying = false;
Vec3 r = v.right(), u = v.up();
if (!v.ortho()) {
if (v.flying) { // right mouse: look + WASD/QE fly
v.yaw -= io.MouseDelta.x * 0.15f;
v.pitch = std::max(-89.0f, std::min(89.0f, v.pitch - io.MouseDelta.y * 0.15f));
float sp = v.flySpeed * dt * (io.KeyShift ? 3.0f : 1.0f);
Vec3 f = v.forward(), mv;
if (ImGui::IsKeyDown(ImGuiKey_W)) mv += f;
if (ImGui::IsKeyDown(ImGuiKey_S)) mv -= f;
if (ImGui::IsKeyDown(ImGuiKey_D)) mv += r;
if (ImGui::IsKeyDown(ImGuiKey_A)) mv -= r;
if (ImGui::IsKeyDown(ImGuiKey_E)) mv += Vec3(0, 1, 0);
if (ImGui::IsKeyDown(ImGuiKey_Q)) mv -= Vec3(0, 1, 0);
v.pos += normalize(mv) * sp;
if (io.MouseWheel != 0) v.flySpeed = std::max(20.0f, std::min(4000.0f, v.flySpeed * std::pow(1.12f, io.MouseWheel)));
} else if (v.hovered && io.MouseWheel != 0) {
v.pos += v.forward() * (io.MouseWheel * std::max(24.0f, v.flySpeed * 0.2f));
}
if (mmb && active) {
float s = std::max(0.25f, v.flySpeed * 0.0035f);
v.pos += (u * io.MouseDelta.y - r * io.MouseDelta.x) * s;
}
} else {
if (v.hovered && io.MouseWheel != 0) { // zoom towards the cursor
Vec3 wp = v.center + r * ((m.x - (float)v.w * 0.5f) / v.zoom) + u * (-(m.y - (float)v.h * 0.5f) / v.zoom);
v.zoom = std::max(0.01f, std::min(24.0f, v.zoom * std::pow(1.18f, io.MouseWheel)));
v.center = wp - r * ((m.x - (float)v.w * 0.5f) / v.zoom) - u * (-(m.y - (float)v.h * 0.5f) / v.zoom);
}
if ((v.flying || mmb) && active) v.center += u * (io.MouseDelta.y / v.zoom) - r * (io.MouseDelta.x / v.zoom);
}
if (v.flying || mmb) return; // navigating: no tool input
if (v.ortho()) toolInput2D(v, m);
else toolInput3D(v, m);
}
void Editor::toolInput3D(Viewport& v, const ImVec2& m) {
ImGuiIO& io = ImGui::GetIO();
bool click = v.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left), released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
if (drag.kind == DragState::Gizmo && drag.view == &v) {
if (down) {
HRay r = v.ray(m.x, m.y);
Vec3 axis(0, 0, 0);
axis[drag.axis] = 1.0f;
float s;
if (axisParam(drag.origin, axis, r, s)) {
float d = snapValue(s - drag.s0);
if (d != 0.0f || drag.moved) {
restoreSnap(drag.snap);
Vec3 delta = axis * d;
if (selMode == SelMode::Vertex) translateSelectedVerts(delta);
else applyMatrix(Mat34::translate(delta), true);
drag.moved = true;
}
}
}
if (released) {
if (!drag.moved && drag.tookUndo && !undoStack.empty()) undoStack.pop_back();
drag.kind = DragState::None;
}
return;
}
if (!click) return;
HRay r = v.ray(m.x, m.y);
if (tool == Tool::Select && gizmoVisible()) {
int ax = gizmoHit(v, m);
if (ax >= 0) {
Vec3 axis(0, 0, 0);
axis[ax] = 1.0f;
drag = DragState();
drag.kind = DragState::Gizmo;
drag.view = &v;
drag.axis = ax;
drag.origin = gizmoCenter();
float s0 = 0;
axisParam(drag.origin, axis, r, s0);
drag.s0 = s0;
drag.snap = captureSnap();
pushUndo();
drag.tookUndo = true;
return;
}
}
Hit h = pick(r);
if (io.KeyAlt && tool != Tool::Entity) { // Alt+click paints the current material
paintHit(h, io.KeyShift);
return;
}
bool add = io.KeyShift || io.KeyCtrl;
switch (tool) {
case Tool::Select: {
if (selMode == SelMode::Vertex) {
ObjRef p = sel.primary();
const hmap::MeshObject* mesh = p.type == ObjType::Mesh ? doc.findMesh(p.id) : nullptr;
int vi = mesh ? nearestVertex(v, *mesh, m, 11.0f) : -1;
if (vi >= 0) {
auto it = std::find(sel.verts.begin(), sel.verts.end(), vi);
if (add) { if (it != sel.verts.end()) sel.verts.erase(it); else sel.verts.push_back(vi); }
else sel.verts = {vi};
} else if (h.hit && h.obj.type == ObjType::Mesh && !(h.obj == p)) {
select(h.obj, false);
} else if (!add) {
sel.verts.clear();
}
} else if (!h.hit) {
if (!add) clearSelection();
} else if (selMode == SelMode::Face && h.obj.type != ObjType::Entity) {
if (sel.primary() == h.obj) {
auto it = std::find(sel.faces.begin(), sel.faces.end(), h.face);
if (add) { if (it != sel.faces.end()) sel.faces.erase(it); else sel.faces.push_back(h.face); }
else sel.faces = {h.face};
} else {
select(h.obj, false);
sel.faces = {h.face};
}
} else {
select(h.obj, add);
}
break;
}
case Tool::Block: {
float s = std::max(grid * 4.0f, 64.0f);
Vec3 sz(s, blockHeight, s), base, n(0, 1, 0);
if (h.hit) { base = h.point; n = dominantAxis(h.normal); }
else if (r.d.y < -1e-4f) base = r.o + r.d * (-r.o.y / r.d.y);
else base = r.o + r.d * 600.0f;
Vec3 c = base;
for (int a = 0; a < 3; ++a)
if (n[a] != 0.0f) c[a] += n[a] * sz[a] * 0.5f;
Vec3 mn(snapValue(c.x - sz.x * 0.5f), snapValue(c.y - sz.y * 0.5f), snapValue(c.z - sz.z * 0.5f));
createPrimitive(mn, mn + sz);
break;
}
case Tool::Entity: {
Vec3 base, n(0, 1, 0);
if (h.hit) { base = h.point; n = dominantAxis(h.normal); }
else if (r.d.y < -1e-4f) base = r.o + r.d * (-r.o.y / r.d.y);
else base = r.o + r.d * 400.0f;
const hmap::EntityClass* c = entdefs.find(curEntity);
Vec3 o = base;
if (c && !c->hasModel)
for (int a = 0; a < 3; ++a)
if (n[a] != 0.0f) o[a] += n[a] * c->half[a];
createEntityAt(o);
break;
}
case Tool::Clip: break; // clipping is done in the 2D views
}
}
void Editor::toolInput2D(Viewport& v, const ImVec2& m) {
ImGuiIO& io = ImGui::GetIO();
bool click = v.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left), released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
int da = v.depthAxis();
Vec3 r = v.right(), u = v.up();
Vec3 depthDir(0, 0, 0);
depthDir[da] = 1.0f;
auto snapped = [&](Vec3 p) {
p[0] = (da == 0) ? p[0] : snapValue(p[0]);
p[1] = (da == 1) ? p[1] : snapValue(p[1]);
p[2] = (da == 2) ? p[2] : snapValue(p[2]);
return p;
};
bool add = io.KeyShift || io.KeyCtrl;
// ---------------------------------------------------- ongoing drag
if (drag.view == &v && drag.kind != DragState::None) {
Vec3 wp = v.planePoint(m.x, m.y, 0);
drag.curScreen = m;
auto finishMove = [&]() {
if (!drag.moved && drag.tookUndo && !undoStack.empty()) undoStack.pop_back();
};
if (drag.kind == DragState::Move) {
if (down) {
Vec3 d = wp - drag.startWorld;
float du = snapValue(dot(d, r)), dv = snapValue(dot(d, u));
if (du != 0.0f || dv != 0.0f || drag.moved) {
restoreSnap(drag.snap);
Vec3 delta = r * du + u * dv;
if (selMode == SelMode::Vertex) translateSelectedVerts(delta);
else applyMatrix(Mat34::translate(delta), true);
drag.moved = true;
}
}
if (released) { finishMove(); drag.kind = DragState::None; }
} else if (drag.kind == DragState::Resize) {
if (down) {
float mu = snapValue(dot(wp, r)), mv = snapValue(dot(wp, u));
float nu0 = drag.rectU0, nu1 = drag.rectU1, nv0 = drag.rectV0, nv1 = drag.rectV1;
float minS = std::max(1.0f, snapOn ? grid : 1.0f);
if (drag.hx < 0) nu0 = std::min(mu, nu1 - minS);
if (drag.hx > 0) nu1 = std::max(mu, nu0 + minS);
if (drag.hy < 0) nv1 = std::max(mv, nv0 + minS); // top handle moves the max-v edge
if (drag.hy > 0) nv0 = std::min(mv, nv1 - minS);
float ou = drag.rectU1 - drag.rectU0, ov = drag.rectV1 - drag.rectV0;
float su = (drag.hx != 0 && ou > 1e-4f) ? (nu1 - nu0) / ou : 1.0f;
float sv = (drag.hy != 0 && ov > 1e-4f) ? (nv1 - nv0) / ov : 1.0f;
float pu = drag.hx < 0 ? drag.rectU1 : drag.rectU0, pv = drag.hy < 0 ? drag.rectV0 : drag.rectV1;
Mat34 M;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) M.m[i][j] = (i == j ? 1.0f : 0.0f) + (su - 1.0f) * r[i] * r[j] + (sv - 1.0f) * u[i] * u[j];
M.m[i][3] = r[i] * pu * (1.0f - su) + u[i] * pv * (1.0f - sv);
}
restoreSnap(drag.snap);
if (selMode == SelMode::Vertex) { // scale the selected vertices about the fixed edge
ObjRef p = sel.primary();
if (auto* mesh = doc.findMesh(p.id))
for (int vi : sel.verts)
if (vi >= 0 && (size_t)vi < mesh->verts.size()) mesh->verts[(size_t)vi] = M.point(mesh->verts[(size_t)vi]);
touch();
} else {
applyMatrix(M, false);
}
drag.moved = true;
}
if (released) { finishMove(); drag.kind = DragState::None; }
} else if (drag.kind == DragState::BoxSelect) {
if (released) {
Vec3 a = v.planePoint(drag.startScreen.x, drag.startScreen.y, 0), b = wp;
float u0 = std::min(dot(a, r), dot(b, r)), u1 = std::max(dot(a, r), dot(b, r));
float v0 = std::min(dot(a, u), dot(b, u)), v1 = std::max(dot(a, u), dot(b, u));
bool tiny = std::fabs(drag.curScreen.x - drag.startScreen.x) < 4 && std::fabs(drag.curScreen.y - drag.startScreen.y) < 4;
if (tiny) {
if (!add) clearSelection();
} else if (selMode == SelMode::Vertex) {
ObjRef p = sel.primary();
if (const hmap::MeshObject* mesh = p.type == ObjType::Mesh ? doc.findMesh(p.id) : nullptr) {
if (!add) sel.verts.clear();
for (size_t i = 0; i < mesh->verts.size(); ++i) {
float pu = dot(mesh->verts[i], r), pv = dot(mesh->verts[i], u);
if (pu >= u0 && pu <= u1 && pv >= v0 && pv <= v1 && std::find(sel.verts.begin(), sel.verts.end(), (int)i) == sel.verts.end())
sel.verts.push_back((int)i);
}
}
} else {
std::vector<ObjRef> picked;
auto inside = [&](const AABB& bb) {
if (bb.empty()) return false;
float a0 = std::min(dot(bb.mn, r), dot(bb.mx, r)), a1 = std::max(dot(bb.mn, r), dot(bb.mx, r));
float b0 = std::min(dot(bb.mn, u), dot(bb.mx, u)), b1 = std::max(dot(bb.mn, u), dot(bb.mx, u));
return a0 >= u0 && a1 <= u1 && b0 >= v0 && b1 <= v1;
};
const SceneCache& sc = scene();
for (size_t i = 0; i < doc.brushes.size(); ++i)
if (!(doc.brushes[i].flags & hmap::OBJ_HIDDEN) && inside(sc.brushGeo[i].bounds)) picked.push_back({ObjType::Brush, doc.brushes[i].id});
for (size_t i = 0; i < doc.meshes.size(); ++i)
if (!(doc.meshes[i].flags & hmap::OBJ_HIDDEN) && inside(sc.meshBounds[i])) picked.push_back({ObjType::Mesh, doc.meshes[i].id});
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
if (inside(entityBounds(e))) picked.push_back({ObjType::Entity, e.id});
}
if (!add) clearSelection();
for (const ObjRef& o : picked)
if (!sel.has(o)) sel.objs.push_back(o);
sel.faces.clear();
sel.verts.clear();
}
drag.kind = DragState::None;
}
} else if (drag.kind == DragState::Block) {
if (down) {
Vec3 p = snapped(wp);
p[da] = 0.0f;
drag.blockB = p;
}
if (released) {
Vec3 a = drag.blockA, b = drag.blockB;
Vec3 mn = hmap::vmin(a, b), mx = hmap::vmax(a, b);
mn[da] = 0.0f;
mx[da] = blockHeight;
float ru = std::fabs(dot(b - a, r)), rv = std::fabs(dot(b - a, u));
if (ru >= 1.0f && rv >= 1.0f) createPrimitive(mn, mx);
drag.kind = DragState::None;
}
}
return;
}
if (!click) return;
// ---------------------------------------------------- new interaction
Vec3 wp = v.planePoint(m.x, m.y, v.center[da]);
switch (tool) {
case Tool::Select: {
float u0, u1, v0, v1;
if (selectionRect(v, u0, u1, v0, v1)) { // resize handles
float L = (float)v.w * 0.5f + (u0 - dot(v.center, r)) * v.zoom, R = (float)v.w * 0.5f + (u1 - dot(v.center, r)) * v.zoom;
float T = (float)v.h * 0.5f - (v1 - dot(v.center, u)) * v.zoom, B = (float)v.h * 0.5f - (v0 - dot(v.center, u)) * v.zoom;
for (int hy = -1; hy <= 1; ++hy)
for (int hx = -1; hx <= 1; ++hx) {
if (!hx && !hy) continue;
float px = hx < 0 ? L : (hx > 0 ? R : (L + R) * 0.5f), py = hy < 0 ? T : (hy > 0 ? B : (T + B) * 0.5f);
if (std::fabs(m.x - px) <= 7 && std::fabs(m.y - py) <= 7) {
drag = DragState();
drag.kind = DragState::Resize;
drag.view = &v;
drag.hx = hx; drag.hy = hy;
drag.rectU0 = u0; drag.rectU1 = u1; drag.rectV0 = v0; drag.rectV1 = v1;
drag.snap = captureSnap();
pushUndo();
drag.tookUndo = true;
return;
}
}
}
HRay ray = v.ray(m.x, m.y);
if (selMode == SelMode::Vertex) {
ObjRef p = sel.primary();
const hmap::MeshObject* mesh = p.type == ObjType::Mesh ? doc.findMesh(p.id) : nullptr;
int vi = mesh ? nearestVertex(v, *mesh, m, 11.0f) : -1;
if (vi >= 0) {
bool selected = std::find(sel.verts.begin(), sel.verts.end(), vi) != sel.verts.end();
if (!selected) {
if (add) sel.verts.push_back(vi);
else sel.verts = {vi};
}
drag = DragState();
drag.kind = DragState::Move;
drag.view = &v;
drag.startWorld = v.planePoint(m.x, m.y, 0);
drag.snap = captureSnap();
pushUndo();
drag.tookUndo = true;
return;
}
Hit h = pick(ray);
if (h.hit && h.obj.type == ObjType::Mesh && !(h.obj == p) && sel.verts.empty()) { select(h.obj, false); return; }
} else {
Hit h = pick(ray);
if (h.hit) {
if (!sel.has(h.obj)) select(h.obj, add);
else if (add) { select(h.obj, true); return; }
drag = DragState();
drag.kind = DragState::Move;
drag.view = &v;
drag.startWorld = v.planePoint(m.x, m.y, 0);
drag.snap = captureSnap();
pushUndo();
drag.tookUndo = true;
return;
}
}
drag = DragState();
drag.kind = DragState::BoxSelect;
drag.view = &v;
drag.startScreen = m;
drag.curScreen = m;
break;
}
case Tool::Block: {
Vec3 p = snapped(v.planePoint(m.x, m.y, 0));
p[da] = 0.0f;
drag = DragState();
drag.kind = DragState::Block;
drag.view = &v;
drag.blockA = drag.blockB = p;
break;
}
case Tool::Entity: {
Vec3 p = snapped(wp);
if (std::fabs(p[da]) < 0.01f && da == 1) p[da] = 0.0f;
createEntityAt(p);
break;
}
case Tool::Clip: {
Vec3 p = snapped(wp);
if (!clipHasA || (clipHasA && clipHasB)) {
clipA = p; clipHasA = true; clipHasB = false;
clipView = (int)(&v - views);
} else if (&v - views == clipView) {
clipB = p; clipHasB = true;
}
break;
}
}
}
// ============================================================ rendering
void Editor::renderView(Viewport& v) {
const SceneCache& sc = scene();
(void)sc;
BeginTextureMode(v.rt);
ClearBackground(v.ortho() ? Color{24, 26, 32, 255} : Color{36, 40, 50, 255});
rlSetClipPlanes(v.ortho() ? 1.0 : 2.0, 100000.0);
BeginMode3D(v.camera());
if (v.ortho()) {
rlDrawRenderBatchActive();
rlDisableDepthTest(); // wire views are painted back-to-front
}
drawGrid(v);
if (!v.ortho()) drawScene3D(v);
drawWire(v);
drawEntities(v);
if (!v.ortho()) drawGizmo(v);
rlDrawRenderBatchActive();
rlEnableDepthTest();
EndMode3D();
drawOverlay2D(v);
EndTextureMode();
}
void Editor::drawGrid(Viewport& v) {
LineBatch lb;
if (v.ortho()) {
Vec3 r = v.right(), u = v.up();
float cu = dot(v.center, r), cv = dot(v.center, u);
float halfU = (float)v.w * 0.5f / v.zoom, halfV = (float)v.h * 0.5f / v.zoom;
float step = std::max(1.0f, grid);
while (step * v.zoom < 9.0f) step *= 2.0f;
float major = step * 8.0f;
int da = v.depthAxis();
auto P = [&](float a, float b) { // a along right, b along up, depth 0
Vec3 p = r * a + u * b;
p[da] = 0.0f;
return p;
};
lb.begin();
for (float a = std::floor((cu - halfU) / step) * step; a <= cu + halfU; a += step) {
bool ax = std::fabs(a) < 0.01f, mj = std::fmod(std::fabs(a), major) < 0.01f;
lb.line(P(a, cv - halfV), P(a, cv + halfV), ax ? Color{110, 60, 60, 255} : (mj ? kGridMajor : kGridMinor));
}
for (float b = std::floor((cv - halfV) / step) * step; b <= cv + halfV; b += step) {
bool ax = std::fabs(b) < 0.01f, mj = std::fmod(std::fabs(b), major) < 0.01f;
lb.line(P(cu - halfU, b), P(cu + halfU, b), ax ? Color{60, 110, 70, 255} : (mj ? kGridMajor : kGridMinor));
}
lb.end();
return;
}
float height = std::max(64.0f, std::fabs(v.pos.y));
float step = std::max(1.0f, grid);
while (step < height / 12.0f) step *= 2.0f;
float major = step * 8.0f, half = step * 40.0f;
float cx = std::floor(v.pos.x / step) * step, cz = std::floor(v.pos.z / step) * step;
lb.begin();
for (int i = -40; i <= 40; ++i) {
float a = cx + (float)i * step, b = cz + (float)i * step;
bool axx = std::fabs(a) < 0.01f, axz = std::fabs(b) < 0.01f;
bool mjx = std::fmod(std::fabs(a), major) < 0.01f, mjz = std::fmod(std::fabs(b), major) < 0.01f;
lb.line({a, 0, cz - half}, {a, 0, cz + half}, axx ? Color{70, 100, 190, 255} : (mjx ? kGridMajor : kGridMinor));
lb.line({cx - half, 0, b}, {cx + half, 0, b}, axz ? Color{190, 80, 80, 255} : (mjz ? kGridMajor : kGridMinor));
}
lb.end();
}
void Editor::drawScene3D(Viewport& v) {
(void)v;
if (shading == Shading::Wire) return;
bool baked = shading == Shading::Baked && hasCompiled && !compiledStale;
hmap::Vec3 ambient = doc.settings.ambient;
for (const hmap::Entity& e : doc.entities)
if (e.classname == "light_environment" && e.find("ambient")) ambient = e.getColor("ambient", ambient);
renderer.setLights(lights, ambient);
int mode = shading == Shading::Flat ? 0 : (baked ? 2 : 1);
renderer.drawBatches(baked, baked ? compiled.materials : doc.materials, assetRoot, mode);
int propMode = shading == Shading::Flat ? 0 : 1;
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if (!c || !c->hasModel || (e.flags & hmap::OBJ_HIDDEN)) continue;
std::string path = e.get("model");
if (path.empty()) continue;
ModelEntry& me = renderer.model(path, assetRoot);
if (!me.ok) continue;
Vec3 tc = e.getColor("tint", Vec3(1, 1, 1));
Color tint = toColor(tc);
if (sel.has({ObjType::Entity, e.id})) tint = Color{255, 205, 170, 255};
renderer.drawModel(me, e.transform(), tint, propMode);
}
}
void Editor::drawWire(Viewport& v) {
const SceneCache& sc = scene();
bool all = v.ortho() || shading == Shading::Wire;
LineBatch lb;
auto brushColor = [&](const hmap::Brush& b) {
if (b.contents & hmap::CONTENTS_TRIGGER) return Color{255, 150, 60, 255};
if (b.contents & hmap::CONTENTS_CLIP) return Color{150, 130, 255, 255};
if (b.contents & hmap::CONTENTS_WATER) return Color{80, 140, 255, 255};
if (b.owner) if (const hmap::Entity* e = doc.findEntity(b.owner)) return toColor(entityColor(*e));
return Color{178, 186, 204, 255};
};
for (int pass = 0; pass < 2; ++pass) { // unselected first, selected on top
lb.begin();
for (size_t i = 0; i < doc.brushes.size(); ++i) {
const hmap::Brush& b = doc.brushes[i];
if (b.flags & hmap::OBJ_HIDDEN) continue;
bool s = sel.has({ObjType::Brush, b.id});
if ((pass == 1) != s || (!all && !s)) continue;
Color col = s ? kSelColor : brushColor(b);
for (const hmap::Polygon& poly : sc.brushGeo[i].polys)
for (size_t k = 0; k < poly.size(); ++k) lb.line(poly[k], poly[(k + 1) % poly.size()], col);
}
for (size_t i = 0; i < doc.meshes.size(); ++i) {
const hmap::MeshObject& m = doc.meshes[i];
if (m.flags & hmap::OBJ_HIDDEN) continue;
bool s = sel.has({ObjType::Mesh, m.id});
if ((pass == 1) != s || (!all && !s)) continue;
Color col = s ? kSelColor : Color{120, 200, 150, 255};
for (const hmap::MeshPoly& p : m.polys)
for (size_t k = 0; k < p.idx.size(); ++k) {
uint32_t a = p.idx[k], b = p.idx[(k + 1) % p.idx.size()];
if (a < m.verts.size() && b < m.verts.size()) lb.line(m.verts[a], m.verts[b], col);
}
}
lb.end();
}
// highlighted faces (Face mode)
ObjRef p = sel.primary();
if (!v.ortho() && selMode == SelMode::Face && !sel.faces.empty()) {
std::vector<hmap::Polygon> polys;
std::vector<Vec3> normals;
if (p.type == ObjType::Brush) {
for (size_t i = 0; i < doc.brushes.size(); ++i)
if (doc.brushes[i].id == p.id)
for (int f : sel.faces)
if (f >= 0 && (size_t)f < sc.brushGeo[i].polys.size() && sc.brushGeo[i].polys[(size_t)f].size() >= 3) {
polys.push_back(sc.brushGeo[i].polys[(size_t)f]);
normals.push_back(doc.brushes[i].faces[(size_t)f].plane.n);
}
} else if (p.type == ObjType::Mesh) {
if (const hmap::MeshObject* m = doc.findMesh(p.id))
for (int f : sel.faces)
if (f >= 0 && (size_t)f < m->polys.size()) {
hmap::Polygon poly;
for (uint32_t k : m->polys[(size_t)f].idx) poly.push_back(m->verts[k]);
if (poly.size() >= 3) { normals.push_back(hmap::polyNormal(poly)); polys.push_back(poly); }
}
}
rlDrawRenderBatchActive();
rlDisableBackfaceCulling();
for (size_t i = 0; i < polys.size(); ++i) {
Vec3 off = normals[i] * 0.15f;
for (size_t k = 1; k + 1 < polys[i].size(); ++k)
DrawTriangle3D(toRl(polys[i][0] + off), toRl(polys[i][k] + off), toRl(polys[i][k + 1] + off), Color{255, 190, 60, 110});
}
rlDrawRenderBatchActive();
rlEnableBackfaceCulling();
lb.begin();
for (size_t i = 0; i < polys.size(); ++i)
for (size_t k = 0; k < polys[i].size(); ++k)
lb.line(polys[i][k] + normals[i] * 0.2f, polys[i][(k + 1) % polys[i].size()] + normals[i] * 0.2f, Color{255, 220, 90, 255});
lb.end();
}
}
void Editor::drawEntities(Viewport& v) {
LineBatch lb;
lb.begin();
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
bool s = sel.has({ObjType::Entity, e.id});
Color col = s ? kSelColor : toColor(entityColor(e));
AABB bb = entityBounds(e);
Vec3 a = bb.mn, b = bb.mx;
Vec3 p[8] = {{a.x, a.y, a.z}, {b.x, a.y, a.z}, {b.x, a.y, b.z}, {a.x, a.y, b.z}, {a.x, b.y, a.z}, {b.x, b.y, a.z}, {b.x, b.y, b.z}, {a.x, b.y, b.z}};
const int ed[12][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {4, 5}, {5, 6}, {6, 7}, {7, 4}, {0, 4}, {1, 5}, {2, 6}, {3, 7}};
for (auto& k : ed) lb.line(p[k[0]], p[k[1]], col);
if (c && c->directional) { // orientation arrow
Vec3 f = e.forward(), up = hmap::cross(f, Vec3(0, 1, 0));
if (length(up) < 0.1f) up = Vec3(1, 0, 0);
up = normalize(up);
float len = std::max(24.0f, c->half.x * 3.0f);
Vec3 tip = e.origin + f * len;
lb.line(e.origin, tip, col);
lb.line(tip, tip - f * (len * 0.22f) + up * (len * 0.1f), col);
lb.line(tip, tip - f * (len * 0.22f) - up * (len * 0.1f), col);
}
if (s && c && (e.classname == "light" || e.classname == "light_spot")) { // light radius
float R = e.getFloat("radius", e.classname == "light" ? 512.0f : 768.0f);
Color rc = Color{255, 220, 120, 150};
for (int i = 0; i < 48; ++i) {
float t0 = (float)i / 48.0f * 6.2831853f, t1 = (float)(i + 1) / 48.0f * 6.2831853f;
lb.line(e.origin + Vec3(std::cos(t0) * R, 0, std::sin(t0) * R), e.origin + Vec3(std::cos(t1) * R, 0, std::sin(t1) * R), rc);
lb.line(e.origin + Vec3(std::cos(t0) * R, std::sin(t0) * R, 0), e.origin + Vec3(std::cos(t1) * R, std::sin(t1) * R, 0), rc);
lb.line(e.origin + Vec3(0, std::cos(t0) * R, std::sin(t0) * R), e.origin + Vec3(0, std::cos(t1) * R, std::sin(t1) * R), rc);
}
}
}
lb.end();
if (!v.ortho() && shading != Shading::Wire) { // translucent solid markers for entities without a visible model
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
if (c && c->hasModel && renderer.model(e.get("model"), assetRoot).ok) continue;
AABB bb = entityBounds(e);
Vec3 sz = bb.size(), cen = bb.center();
DrawCube(toRl(cen), sz.x, sz.y, sz.z, toColor(entityColor(e), 0.30f));
}
}
}
void Editor::drawGizmo(Viewport& v) {
if (!gizmoVisible()) return;
Vec3 c = gizmoCenter();
float L = gizmoLength(v, c);
int hot = drag.kind == DragState::Gizmo && drag.view == &v ? drag.axis : -1;
if (hot < 0 && v.hovered && !v.flying) {
ImGuiIO& io = ImGui::GetIO();
hot = gizmoHit(v, ImVec2(io.MousePos.x - v.imgMin.x, io.MousePos.y - v.imgMin.y));
}
rlDrawRenderBatchActive();
rlDisableDepthTest();
const Color cols[3] = {{235, 70, 70, 255}, {90, 220, 90, 255}, {80, 130, 255, 255}};
for (int a = 0; a < 3; ++a) {
Vec3 d(0, 0, 0);
d[a] = 1.0f;
Vec3 tip = c + d * L;
Color col = a == hot ? Color{255, 240, 90, 255} : cols[a];
DrawCylinderEx(toRl(c), toRl(tip - d * (L * 0.2f)), L * 0.012f, L * 0.012f, 6, col);
DrawCylinderEx(toRl(tip - d * (L * 0.22f)), toRl(tip), L * 0.055f, 0.0f, 10, col);
}
DrawCube(toRl(c), L * 0.06f, L * 0.06f, L * 0.06f, Color{240, 240, 240, 255});
rlDrawRenderBatchActive();
rlEnableDepthTest();
}
void Editor::drawOverlay2D(Viewport& v) {
// vertex handles (mesh vertex mode)
ObjRef p = sel.primary();
if (selMode == SelMode::Vertex && p.type == ObjType::Mesh) {
if (const hmap::MeshObject* m = doc.findMesh(p.id)) {
for (size_t i = 0; i < m->verts.size(); ++i) {
float sx, sy;
if (!v.project(m->verts[i], sx, sy)) continue;
bool s = std::find(sel.verts.begin(), sel.verts.end(), (int)i) != sel.verts.end();
DrawRectangle((int)sx - 3, (int)sy - 3, 7, 7, s ? kSelColor : kVertColor);
DrawRectangleLines((int)sx - 3, (int)sy - 3, 7, 7, Color{0, 0, 0, 200});
}
}
}
if (!v.ortho()) return;
Vec3 r = v.right(), u = v.up();
int da = v.depthAxis();
// resize handles around the selection
float u0, u1, v0, v1;
if (tool == Tool::Select && selectionRect(v, u0, u1, v0, v1)) {
float L = (float)v.w * 0.5f + (u0 - dot(v.center, r)) * v.zoom, R = (float)v.w * 0.5f + (u1 - dot(v.center, r)) * v.zoom;
float T = (float)v.h * 0.5f - (v1 - dot(v.center, u)) * v.zoom, B = (float)v.h * 0.5f - (v0 - dot(v.center, u)) * v.zoom;
DrawRectangleLines((int)L, (int)T, (int)(R - L), (int)(B - T), Color{255, 128, 48, 200});
for (int hy = -1; hy <= 1; ++hy)
for (int hx = -1; hx <= 1; ++hx) {
if (!hx && !hy) continue;
float px = hx < 0 ? L : (hx > 0 ? R : (L + R) * 0.5f), py = hy < 0 ? T : (hy > 0 ? B : (T + B) * 0.5f);
DrawRectangle((int)px - 4, (int)py - 4, 9, 9, Color{255, 200, 90, 255});
DrawRectangleLines((int)px - 4, (int)py - 4, 9, 9, Color{40, 20, 0, 255});
}
}
// entity labels
if (v.zoom > 0.22f)
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
float sx, sy;
if (!v.project(e.origin, sx, sy)) continue;
std::string label = e.classname;
std::string name = e.get("targetname");
if (!name.empty()) label += " [" + name + "]";
DrawText(label.c_str(), (int)sx + 8, (int)sy - 14, 10, Color{190, 196, 212, 220});
}
// box select rectangle
if (drag.view == &v && drag.kind == DragState::BoxSelect) {
float x0 = std::min(drag.startScreen.x, drag.curScreen.x), y0 = std::min(drag.startScreen.y, drag.curScreen.y);
float x1 = std::max(drag.startScreen.x, drag.curScreen.x), y1 = std::max(drag.startScreen.y, drag.curScreen.y);
DrawRectangle((int)x0, (int)y0, (int)(x1 - x0), (int)(y1 - y0), Color{255, 160, 60, 40});
DrawRectangleLines((int)x0, (int)y0, (int)(x1 - x0), (int)(y1 - y0), Color{255, 160, 60, 255});
}
// block ghost
if (drag.view == &v && drag.kind == DragState::Block) {
float ax, ay, bx, by;
v.project(drag.blockA, ax, ay);
v.project(drag.blockB, bx, by);
float x0 = std::min(ax, bx), y0 = std::min(ay, by), x1 = std::max(ax, bx), y1 = std::max(ay, by);
DrawRectangle((int)x0, (int)y0, (int)(x1 - x0), (int)(y1 - y0), Color{90, 190, 255, 45});
DrawRectangleLines((int)x0, (int)y0, (int)(x1 - x0), (int)(y1 - y0), Color{90, 190, 255, 255});
char buf[64];
std::snprintf(buf, sizeof buf, "%.0f x %.0f", std::fabs(dot(drag.blockB - drag.blockA, r)), std::fabs(dot(drag.blockB - drag.blockA, u)));
DrawText(buf, (int)x0 + 4, (int)y0 - 14, 12, Color{140, 215, 255, 255});
}
// clip line
if (tool == Tool::Clip && clipHasA && (&v - views) == clipView) {
float ax, ay;
v.project(clipA, ax, ay);
DrawCircle((int)ax, (int)ay, 4, Color{255, 90, 90, 255});
if (clipHasB) {
float bx, by;
v.project(clipB, bx, by);
DrawLine((int)ax, (int)ay, (int)bx, (int)by, Color{255, 90, 90, 255});
DrawCircle((int)bx, (int)by, 4, Color{255, 90, 90, 255});
// arrow on the side that is kept
Vec3 depthDir(0, 0, 0);
depthDir[da] = 1.0f;
Vec3 n = normalize(hmap::cross(clipB - clipA, depthDir));
float nx = dot(n, r), ny = -dot(n, u); // screen-space direction of the plane normal
float mx = (ax + bx) * 0.5f, my = (ay + by) * 0.5f;
float sgn = clipMode == 1 ? -1.0f : 1.0f;
if (clipMode != 2) DrawLine((int)mx, (int)my, (int)(mx + nx * sgn * 28), (int)(my + ny * sgn * 28), Color{255, 220, 90, 255});
DrawText(clipMode == 0 ? "keep front (Tab: switch)" : (clipMode == 1 ? "keep back (Tab: switch)" : "keep both (Tab: switch)"), (int)mx + 8, (int)my + 8, 12,
Color{255, 220, 90, 255});
}
}
}
} // namespace hpp