#include #include #include #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 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 polys; std::vector 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