#include "editor.h" #include #include #include #include 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, /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 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 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& 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 created; for (const ObjRef& r : std::vector(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 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 next; int hollowed = 0; for (uint32_t id : ids) { const hmap::Brush* b = doc.findBrush(id); if (!b) continue; std::vector 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 ids; for (const ObjRef& r : sel.objs) if (r.type == ObjType::Brush) ids.push_back(r.id); if (ids.empty()) return; pushUndo(); std::vector 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 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 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 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 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 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