1003 lines
45 KiB
C++
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
|