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

791 lines
29 KiB
C++

#include "editor.h"
#include <algorithm>
#include <cmath>
#include <cstdarg>
#include <cstdio>
namespace hpp {
bool Selection::has(const ObjRef& r) const {
for (const ObjRef& o : objs)
if (o == r) return true;
return false;
}
void Editor::log(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
std::vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
logLines.push_back(buf);
if (logLines.size() > 500) logLines.erase(logLines.begin(), logLines.begin() + 100);
TraceLog(LOG_INFO, "[Hammer++] %s", buf);
}
// ============================================================ lifecycle
void Editor::init() {
renderer.init();
entdefs = hmap::EntityRegistry::builtin();
// locate the asset folder: ./assets, <exe>/assets, or the source tree it was built from
std::string appAssets = std::string(GetApplicationDirectory()) + "assets";
if (DirectoryExists("assets")) assetRoot = "assets";
else if (DirectoryExists(appAssets.c_str())) assetRoot = appAssets;
#ifdef HPP_ASSET_DIR
else assetRoot = HPP_ASSET_DIR;
#endif
std::string defsPath = assetRoot + "/entities.def";
if (FileExists(defsPath.c_str())) {
char* txt = LoadFileText(defsPath.c_str());
if (txt) {
hmap::Status s = entdefs.parse(txt);
UnloadFileText(txt);
if (!s) log("entities.def: %s", s.message.c_str());
else log("Loaded custom entity definitions from %s", defsPath.c_str());
}
}
const char* names[4] = {"3D", "Top", "Front", "Side"};
const ViewKind kinds[4] = {ViewKind::Persp, ViewKind::Top, ViewKind::Front, ViewKind::Side};
for (int i = 0; i < 4; ++i) {
views[i].title = names[i];
views[i].kind = kinds[i];
views[i].center = {0, 64, 0};
views[i].zoom = 0.4f;
}
views[0].pos = {320, 260, 420};
views[0].yaw = 38;
views[0].pitch = -22;
newMap(true);
log("Hammer++ ready. Right-mouse + WASD flies the 3D view; F1 shows all controls.");
}
void Editor::shutdown() {
for (Viewport& v : views)
if (v.rt.id != 0) UnloadRenderTexture(v.rt);
renderer.shutdown();
}
void Editor::touch() {
++docVersion;
dirty = true;
compiledStale = hasCompiled;
}
void Editor::pushUndo() {
undoStack.push_back(doc);
if (undoStack.size() > 48) undoStack.erase(undoStack.begin());
redoStack.clear();
}
void Editor::editBegin() {
if (!inEdit) {
pushUndo();
inEdit = true;
}
}
void Editor::undo() {
if (undoStack.empty()) return;
redoStack.push_back(doc);
doc = std::move(undoStack.back());
undoStack.pop_back();
touch();
pruneSelection();
}
void Editor::redo() {
if (redoStack.empty()) return;
undoStack.push_back(doc);
doc = std::move(redoStack.back());
redoStack.pop_back();
touch();
pruneSelection();
}
// ============================================================ documents
void Editor::newMap(bool demo) {
if (demo) {
doc = hmap::makeDemoMap();
} else {
doc = hmap::Map();
hmap::addDefaultMaterials(doc);
int wall = doc.findMaterial("dev/wall"), floorM = doc.findMaterial("dev/grid");
hmap::Brush shell = hmap::makeBox({-256, -16, -256}, {256, 208, 256}, wall);
for (hmap::Brush& w : hmap::hollowBrush(shell, 16)) {
for (hmap::Face& f : w.faces)
if (f.plane.n.y > 0.9f && std::fabs(f.plane.d) < 0.5f) f.material = floorM;
w.id = doc.newId();
doc.brushes.push_back(w);
}
auto addEnt = [&](const char* cls, Vec3 o, Vec3 a) {
hmap::Entity e;
e.id = doc.newId();
e.classname = cls;
e.origin = o;
e.angles = a;
entdefs.applyDefaults(e);
doc.entities.push_back(e);
};
addEnt("light", {0, 150, 0}, {0, 0, 0});
addEnt("info_player_start", {0, 36, 128}, {0, 0, 0});
addEnt("light_environment", {0, 200, 0}, {-50, 30, 0});
}
docPath.clear();
undoStack.clear();
redoStack.clear();
sel.clear();
hasCompiled = compiledStale = false;
curMaterial = std::max(0, doc.findMaterial("dev/grid"));
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
touch();
dirty = false;
cache.version = 0;
}
bool Editor::openMap(const std::string& path) {
hmap::Map m;
hmap::Status s = hmap::loadMap(path, m);
if (!s) {
log("Open failed: %s", s.message.c_str());
return false;
}
doc = std::move(m);
docPath = path;
undoStack.clear();
redoStack.clear();
sel.clear();
hasCompiled = compiledStale = false;
if (doc.materials.empty()) hmap::addDefaultMaterials(doc);
curMaterial = std::min(curMaterial, (int)doc.materials.size() - 1);
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
touch();
dirty = false;
cache.version = 0;
log("Opened %s (%zu brushes, %zu meshes, %zu entities)", path.c_str(), doc.brushes.size(), doc.meshes.size(), doc.entities.size());
return true;
}
bool Editor::saveMap(const std::string& path) {
doc.settings.gridSize = grid;
hmap::Status s = hmap::saveMap(path, doc, saveCompiledToo ? &compiled : nullptr);
if (!s) {
log("Save failed: %s", s.message.c_str());
return false;
}
docPath = path;
dirty = false;
log("Saved %s", path.c_str());
return true;
}
void Editor::compileNow() {
compiled = hmap::compile(doc, compileOpt);
hasCompiled = true;
compiledStale = false;
renderer.setCompiledBatches(compiled.batches);
const hmap::CompileStats& st = compiled.stats;
log("Compiled in %.0f ms: %u brushes, %u meshes, %u triangles, BSP %u nodes / %u leaves (depth %u), %u lights%s", st.ms, st.brushes,
st.meshes, st.triangles, st.bspNodes, st.bspLeaves, st.bspDepth, st.lights, compiled.baked ? ", lighting baked" : "");
if (compileOpt.bakeLighting) shading = Shading::Baked;
}
void Editor::exportFbx(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::exportFBX(path, cm, fbxOpt);
log(s ? "Exported FBX: %s (%u triangles)" : "FBX export failed: %s", s ? path.c_str() : s.message.c_str(), cm.stats.triangles);
}
void Editor::exportObj(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::exportOBJ(path, cm, fbxOpt.scale);
log(s ? "Exported OBJ: %s" : "OBJ export failed: %s", s ? path.c_str() : s.message.c_str());
}
void Editor::exportCompiled(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::saveCompiled(path, cm);
log(s ? "Exported runtime map: %s" : "Export failed: %s", s ? path.c_str() : s.message.c_str());
}
// ============================================================ scene cache / picking
const SceneCache& Editor::scene() {
if (cache.version != docVersion) {
cache.brushGeo.resize(doc.brushes.size());
for (size_t i = 0; i < doc.brushes.size(); ++i) cache.brushGeo[i] = hmap::buildBrushGeometry(doc.brushes[i]);
cache.meshBounds.resize(doc.meshes.size());
for (size_t i = 0; i < doc.meshes.size(); ++i) cache.meshBounds[i] = hmap::meshBounds(doc.meshes[i]);
cache.version = docVersion;
refreshGpu();
}
return cache;
}
void Editor::refreshGpu() {
std::vector<hmap::Batch> batches;
hmap::buildEditBatches(doc, batches);
renderer.setEditBatches(batches);
lights = hmap::collectLights(doc);
}
AABB Editor::entityBounds(const hmap::Entity& e) {
const hmap::EntityClass* c = classOf(e);
Vec3 half = c ? c->half : Vec3(8, 8, 8);
AABB bb;
if (c && c->hasModel) {
std::string path = e.get("model");
if (!path.empty()) {
ModelEntry& me = renderer.model(path, assetRoot);
if (me.ok) {
Mat34 xf = e.transform();
for (int i = 0; i < 8; ++i) {
Vec3 p((i & 1) ? me.box.max.x : me.box.min.x, (i & 2) ? me.box.max.y : me.box.min.y, (i & 4) ? me.box.max.z : me.box.min.z);
bb.add(xf.point(p));
}
return bb;
}
}
}
Vec3 s(std::fabs(e.scale.x), std::fabs(e.scale.y), std::fabs(e.scale.z));
Vec3 h(half.x * (c && c->hasModel ? s.x : 1.0f), half.y * (c && c->hasModel ? s.y : 1.0f), half.z * (c && c->hasModel ? s.z : 1.0f));
bb.add(e.origin - h);
bb.add(e.origin + h);
return bb;
}
Vec3 Editor::entityColor(const hmap::Entity& e) const {
const hmap::EntityClass* c = classOf(e);
return c ? c->color : Vec3(0.8f, 0.8f, 0.8f);
}
AABB Editor::objectBounds(const ObjRef& r) {
const SceneCache& sc = scene();
switch (r.type) {
case ObjType::Brush:
for (size_t i = 0; i < doc.brushes.size(); ++i)
if (doc.brushes[i].id == r.id) return sc.brushGeo[i].bounds;
break;
case ObjType::Mesh:
for (size_t i = 0; i < doc.meshes.size(); ++i)
if (doc.meshes[i].id == r.id) return sc.meshBounds[i];
break;
case ObjType::Entity:
if (const hmap::Entity* e = doc.findEntity(r.id)) return entityBounds(*e);
break;
}
return AABB();
}
AABB Editor::selectionBounds() {
AABB bb;
for (const ObjRef& r : sel.objs) bb.add(objectBounds(r));
return bb;
}
Vec3 Editor::selectionCenter() {
AABB bb = selectionBounds();
return bb.empty() ? Vec3() : bb.center();
}
Hit Editor::pick(const HRay& r) {
Hit best;
const SceneCache& sc = scene();
for (size_t i = 0; i < doc.brushes.size(); ++i) {
const hmap::Brush& b = doc.brushes[i];
if (b.flags & hmap::OBJ_HIDDEN) continue;
const AABB& bb = sc.brushGeo[i].bounds;
float tn, tf;
if (bb.empty() || !hmap::rayAABB(r.o, r.d, bb.expanded(0.05f), tn, tf)) continue;
float t;
int f;
if (hmap::rayBrush(b, r.o, r.d, t, f) && t < best.t) {
best.hit = true; best.t = t; best.obj = {ObjType::Brush, b.id}; best.face = f; best.normal = b.faces[(size_t)f].plane.n;
}
}
for (size_t i = 0; i < doc.meshes.size(); ++i) {
const hmap::MeshObject& m = doc.meshes[i];
if (m.flags & hmap::OBJ_HIDDEN) continue;
float tn, tf;
if (sc.meshBounds[i].empty() || !hmap::rayAABB(r.o, r.d, sc.meshBounds[i].expanded(0.05f), tn, tf)) continue;
float t;
int p;
if (hmap::rayMesh(m, r.o, r.d, t, p) && t < best.t) {
hmap::Polygon poly;
for (uint32_t k : m.polys[(size_t)p].idx) poly.push_back(m.verts[k]);
best.hit = true; best.t = t; best.obj = {ObjType::Mesh, m.id}; best.face = p; best.normal = hmap::polyNormal(poly);
}
}
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
AABB bb = entityBounds(e);
float tn, tf;
if (!hmap::rayAABB(r.o, r.d, bb, tn, tf)) continue;
float t = std::max(tn, 0.0f);
if (t < best.t) {
best.hit = true; best.t = t; best.obj = {ObjType::Entity, e.id}; best.face = -1; best.normal = {0, 1, 0};
}
}
if (best.hit) best.point = r.o + r.d * best.t;
return best;
}
// ============================================================ selection
void Editor::clearSelection() { sel.clear(); }
void Editor::select(const ObjRef& r, bool additive) {
if (!additive) {
sel.clear();
sel.objs.push_back(r);
return;
}
auto it = std::find(sel.objs.begin(), sel.objs.end(), r);
if (it != sel.objs.end()) sel.objs.erase(it);
else sel.objs.push_back(r);
sel.faces.clear();
sel.verts.clear();
}
void Editor::selectAll() {
sel.clear();
for (const hmap::Brush& b : doc.brushes)
if (!(b.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Brush, b.id});
for (const hmap::MeshObject& m : doc.meshes)
if (!(m.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Mesh, m.id});
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if (!(c && c->brush) && !(e.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Entity, e.id});
}
}
void Editor::pruneSelection() {
std::vector<ObjRef> keep;
for (const ObjRef& r : sel.objs) {
bool ok = r.type == ObjType::Brush ? doc.findBrush(r.id) != nullptr
: r.type == ObjType::Mesh ? doc.findMesh(r.id) != nullptr
: doc.findEntity(r.id) != nullptr;
if (ok) keep.push_back(r);
}
sel.objs = keep;
ObjRef p = sel.primary();
int limit = 0;
if (p.type == ObjType::Brush && doc.findBrush(p.id)) limit = (int)doc.findBrush(p.id)->faces.size();
if (p.type == ObjType::Mesh && doc.findMesh(p.id)) limit = (int)doc.findMesh(p.id)->polys.size();
sel.faces.erase(std::remove_if(sel.faces.begin(), sel.faces.end(), [&](int f) { return f >= limit; }), sel.faces.end());
int vlimit = (p.type == ObjType::Mesh && doc.findMesh(p.id)) ? (int)doc.findMesh(p.id)->verts.size() : 0;
sel.verts.erase(std::remove_if(sel.verts.begin(), sel.verts.end(), [&](int v) { return v >= vlimit; }), sel.verts.end());
}
bool Editor::selectedBrushes(std::vector<hmap::Brush*>& out) {
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush)
if (hmap::Brush* b = doc.findBrush(r.id)) out.push_back(b);
return !out.empty();
}
// ============================================================ edit commands
static void pruneOrphanEntities(Editor& ed) {
auto used = [&](uint32_t id) {
for (const hmap::Brush& b : ed.doc.brushes) if (b.owner == id) return true;
for (const hmap::MeshObject& m : ed.doc.meshes) if (m.owner == id) return true;
return false;
};
auto& es = ed.doc.entities;
es.erase(std::remove_if(es.begin(), es.end(),
[&](const hmap::Entity& e) {
const hmap::EntityClass* c = ed.classOf(e);
return c && c->brush && !used(e.id);
}),
es.end());
}
void Editor::deleteSelection() {
if (sel.empty()) return;
pushUndo();
for (const ObjRef& r : sel.objs) {
auto sameId = [&](const auto& o) { return o.id == r.id; };
if (r.type == ObjType::Brush) doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), sameId), doc.brushes.end());
else if (r.type == ObjType::Mesh) doc.meshes.erase(std::remove_if(doc.meshes.begin(), doc.meshes.end(), sameId), doc.meshes.end());
else doc.entities.erase(std::remove_if(doc.entities.begin(), doc.entities.end(), sameId), doc.entities.end());
}
pruneOrphanEntities(*this);
sel.clear();
touch();
}
void Editor::duplicateSelection() {
if (sel.empty()) return;
pushUndo();
Vec3 off(grid, 0, grid);
std::vector<ObjRef> created;
for (const ObjRef& r : std::vector<ObjRef>(sel.objs)) {
if (r.type == ObjType::Brush) {
if (const hmap::Brush* src = doc.findBrush(r.id)) {
hmap::Brush b = *src;
b.id = doc.newId();
hmap::translateBrush(b, off, true);
doc.brushes.push_back(b);
created.push_back({ObjType::Brush, b.id});
}
} else if (r.type == ObjType::Mesh) {
if (const hmap::MeshObject* src = doc.findMesh(r.id)) {
hmap::MeshObject m = *src;
m.id = doc.newId();
for (Vec3& v : m.verts) v += off;
doc.meshes.push_back(m);
created.push_back({ObjType::Mesh, m.id});
}
} else if (const hmap::Entity* src = doc.findEntity(r.id)) {
hmap::Entity e = *src;
e.id = doc.newId();
e.origin += off;
doc.entities.push_back(e);
created.push_back({ObjType::Entity, e.id});
}
}
sel.clear();
sel.objs = created;
touch();
log("Duplicated %zu object(s)", created.size());
}
void Editor::hideSelection(bool hide) {
for (const ObjRef& r : sel.objs) {
uint32_t* flags = nullptr;
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) flags = &b->flags; }
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) flags = &m->flags; }
else if (auto* e = doc.findEntity(r.id)) flags = &e->flags;
if (!flags) continue;
if (hide) *flags |= hmap::OBJ_HIDDEN;
else *flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
}
if (hide) sel.clear();
touch();
}
void Editor::unhideAll() {
for (auto& b : doc.brushes) b.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
for (auto& m : doc.meshes) m.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
for (auto& e : doc.entities) e.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
touch();
}
void Editor::hollowSelection(float thickness) {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) { log("Hollow: select a brush first"); return; }
pushUndo();
std::vector<ObjRef> next;
int hollowed = 0;
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
std::vector<hmap::Brush> walls = hmap::hollowBrush(*b, thickness);
if (walls.empty()) continue;
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
for (hmap::Brush& w : walls) {
w.id = doc.newId();
doc.brushes.push_back(w);
next.push_back({ObjType::Brush, w.id});
}
++hollowed;
}
if (!hollowed) { undoStack.pop_back(); log("Hollow failed: wall thickness is too large for this brush"); return; }
sel.clear();
sel.objs = next;
touch();
log("Hollowed %d brush(es) into %zu wall brushes", hollowed, next.size());
}
void Editor::convertSelectionToMesh() {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) return;
pushUndo();
std::vector<ObjRef> next;
for (const ObjRef& r : sel.objs)
if (r.type != ObjType::Brush) next.push_back(r);
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
hmap::MeshObject m = hmap::brushToMesh(*b);
m.id = doc.newId();
m.name = "mesh " + std::to_string(m.id);
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
next.push_back({ObjType::Mesh, m.id});
doc.meshes.push_back(std::move(m));
}
sel.clear();
sel.objs = next;
touch();
log("Converted %zu brush(es) to editable meshes", ids.size());
}
void Editor::tieSelectionToEntity(const std::string& cls) {
const hmap::EntityClass* c = entdefs.find(cls);
if (!c || !c->brush) return;
std::vector<ObjRef> targets;
for (const ObjRef& r : sel.objs)
if (r.type != ObjType::Entity) targets.push_back(r);
if (targets.empty()) { log("Tie to entity: select some brushes or meshes first"); return; }
pushUndo();
hmap::Entity e;
e.id = doc.newId();
e.classname = cls;
entdefs.applyDefaults(e);
uint32_t eid = e.id;
doc.entities.push_back(e);
for (const ObjRef& r : targets) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) { b->owner = eid; b->contents = c->brushContents; }
} else if (auto* m = doc.findMesh(r.id)) m->owner = eid;
}
pruneOrphanEntities(*this);
touch();
log("Tied %zu object(s) to new %s", targets.size(), cls.c_str());
}
void Editor::untieSelection() {
pushUndo();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) { b->owner = 0; b->contents = hmap::CONTENTS_SOLID; }
} else if (r.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(r.id)) m->owner = 0;
}
}
pruneOrphanEntities(*this);
touch();
}
void Editor::clipSelection(const Plane& p, int mode) {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) { log("Clip: select the brush(es) to cut first"); return; }
pushUndo();
std::vector<ObjRef> next;
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
hmap::Brush front, back;
hmap::clipBrush(*b, p, &front, &back);
std::vector<hmap::Brush> keep;
if (!front.faces.empty() && mode != 1) keep.push_back(front);
if (!back.faces.empty() && mode != 0) keep.push_back(back);
if (keep.empty() || (front.faces.empty() && back.faces.empty())) continue;
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
for (hmap::Brush& k : keep) {
k.id = doc.newId();
doc.brushes.push_back(k);
next.push_back({ObjType::Brush, k.id});
}
}
sel.clear();
sel.objs = next;
touch();
}
void Editor::createPrimitive(Vec3 mn, Vec3 mx) {
for (int i = 0; i < 3; ++i) {
if (mx[i] < mn[i]) std::swap(mx[i], mn[i]);
if (mx[i] - mn[i] < 1.0f) mx[i] = mn[i] + std::max(grid, 1.0f);
}
pushUndo();
std::vector<ObjRef> created;
int mat = curMaterial;
auto addB = [&](hmap::Brush b) {
b.id = doc.newId();
created.push_back({ObjType::Brush, b.id});
doc.brushes.push_back(std::move(b));
};
auto addM = [&](hmap::MeshObject m, const char* name) {
m.id = doc.newId();
m.name = name;
created.push_back({ObjType::Mesh, m.id});
doc.meshes.push_back(std::move(m));
};
bool asMesh = createAsMesh || shape == Shape::Sphere || shape == Shape::Torus || shape == Shape::Terrain;
auto brushOrMesh = [&](hmap::Brush b, const char* name) {
if (asMesh) addM(hmap::brushToMesh(b), name);
else addB(std::move(b));
};
switch (shape) {
case Shape::Box:
if (asMesh) addM(hmap::meshBox(mn, mx, mat), "box");
else addB(hmap::makeBox(mn, mx, mat));
break;
case Shape::Wedge: brushOrMesh(hmap::makeWedge(mn, mx, mat), "wedge"); break;
case Shape::Cylinder:
if (asMesh) addM(hmap::meshCylinder(mn, mx, shapeSides, mat), "cylinder");
else addB(hmap::makeCylinder(mn, mx, shapeSides, mat));
break;
case Shape::Cone:
if (asMesh) addM(hmap::meshCone(mn, mx, shapeSides, mat), "cone");
else addB(hmap::makeCone(mn, mx, shapeSides, mat));
break;
case Shape::Stairs:
for (hmap::Brush& s : hmap::makeStairs(mn, mx, stairSteps, mat)) brushOrMesh(std::move(s), "step");
break;
case Shape::Sphere: addM(hmap::meshSphere(mn, mx, std::max(4, shapeSides / 2), shapeSides, mat), "sphere"); break;
case Shape::Torus: addM(hmap::meshTorus(mn, mx, shapeSides * 2, 12, mat), "torus"); break;
case Shape::Terrain: addM(hmap::meshGrid(mn, mx, 16, 16, mat), "terrain"); break;
}
sel.clear();
sel.objs = created;
touch();
}
void Editor::createEntityAt(const Vec3& origin) {
const hmap::EntityClass* c = entdefs.find(curEntity);
if (!c) return;
if (c->brush) { log("%s is a brush entity: select brushes and use Tie to entity (Ctrl+T)", curEntity.c_str()); return; }
pushUndo();
hmap::Entity e;
e.id = doc.newId();
e.classname = curEntity;
e.origin = origin;
entdefs.applyDefaults(e);
if (c->hasModel && !curModel.empty()) e.set("model", curModel);
doc.entities.push_back(e);
sel.clear();
sel.objs.push_back({ObjType::Entity, e.id});
touch();
}
void Editor::applyMaterialToSelection(int material) {
if (sel.empty()) return;
pushUndo();
ObjRef p = sel.primary();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) {
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
for (size_t i = 0; i < b->faces.size(); ++i)
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) b->faces[i].material = material;
}
} else if (r.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(r.id)) {
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
for (size_t i = 0; i < m->polys.size(); ++i)
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) m->polys[i].material = material;
}
}
}
touch();
}
void Editor::paintHit(const Hit& h, bool wholeObject) {
if (!h.hit) return;
pushUndo();
if (h.obj.type == ObjType::Brush) {
if (auto* b = doc.findBrush(h.obj.id))
for (size_t i = 0; i < b->faces.size(); ++i)
if (wholeObject || (int)i == h.face) b->faces[i].material = curMaterial;
} else if (h.obj.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(h.obj.id))
for (size_t i = 0; i < m->polys.size(); ++i)
if (wholeObject || (int)i == h.face) m->polys[i].material = curMaterial;
} else {
undoStack.pop_back();
return;
}
touch();
}
void Editor::flipSelection() {
pushUndo();
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Mesh)
if (auto* m = doc.findMesh(r.id)) hmap::flipMesh(*m);
touch();
}
void Editor::importObjFile(const std::string& path) {
pushUndo();
hmap::MeshObject m;
hmap::Status s = hmap::importOBJ(path, doc, m);
if (!s) {
undoStack.pop_back();
log("OBJ import failed: %s", s.message.c_str());
return;
}
m.id = doc.newId();
sel.clear();
sel.objs.push_back({ObjType::Mesh, m.id});
log("Imported %s as mesh '%s' (%zu verts, %zu polys)", path.c_str(), m.name.c_str(), m.verts.size(), m.polys.size());
doc.meshes.push_back(std::move(m));
touch();
}
// ============================================================ drag support
DragSnap Editor::captureSnap() {
DragSnap s;
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) s.brushes.push_back(*b); }
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) s.meshes.push_back(*m); }
else if (auto* e = doc.findEntity(r.id)) s.entities.push_back(*e);
}
return s;
}
void Editor::restoreSnap(const DragSnap& s) {
for (const auto& b : s.brushes) if (auto* p = doc.findBrush(b.id)) *p = b;
for (const auto& m : s.meshes) if (auto* p = doc.findMesh(m.id)) *p = m;
for (const auto& e : s.entities) if (auto* p = doc.findEntity(e.id)) *p = e;
}
static bool isTranslation(const Mat34& m) {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (std::fabs(m.m[i][j] - (i == j ? 1.0f : 0.0f)) > 1e-5f) return false;
return true;
}
void Editor::applyMatrix(const Mat34& m, bool lockTexture) {
bool pure = isTranslation(m);
Vec3 t = m.translation();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) {
if (pure) hmap::translateBrush(*b, t, lockTexture);
else hmap::transformBrush(*b, m);
}
} else if (r.type == ObjType::Mesh) {
if (auto* me = doc.findMesh(r.id)) hmap::transformMesh(*me, m);
} else if (auto* e = doc.findEntity(r.id)) {
e->origin = m.point(e->origin);
if (pure) continue;
Vec3 c0(m.m[0][0], m.m[1][0], m.m[2][0]), c1(m.m[0][1], m.m[1][1], m.m[2][1]), c2(m.m[0][2], m.m[1][2], m.m[2][2]);
Vec3 ls(hmap::length(c0), hmap::length(c1), hmap::length(c2));
if (ls.x > 1e-6f && ls.y > 1e-6f && ls.z > 1e-6f) {
Mat34 rot;
for (int i = 0; i < 3; ++i) {
rot.m[i][0] = m.m[i][0] / ls.x;
rot.m[i][1] = m.m[i][1] / ls.y;
rot.m[i][2] = m.m[i][2] / ls.z;
}
if (rot.det3() > 0) e->angles = (rot * Mat34::fromEuler(e->angles)).toEuler();
if (std::fabs(ls.x - 1) > 1e-4f || std::fabs(ls.y - 1) > 1e-4f || std::fabs(ls.z - 1) > 1e-4f)
e->scale = Vec3(e->scale.x * ls.x, e->scale.y * ls.y, e->scale.z * ls.z);
}
}
}
touch();
}
void Editor::translateSelectedVerts(const Vec3& d) {
ObjRef p = sel.primary();
if (p.type != ObjType::Mesh) return;
if (auto* m = doc.findMesh(p.id))
for (int v : sel.verts)
if (v >= 0 && (size_t)v < m->verts.size()) m->verts[(size_t)v] += d;
touch();
}
} // namespace hpp