载入中...
搜索中...
未找到
ArchSpaceDocument.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <unordered_map>
8#include <utility>
9
10namespace eve::archspace {
11namespace {
12
13constexpr double kEps = 1e-8;
14
16
17bool isFinite(double v) { return std::isfinite(v); }
18
19bool isFinite(const Vec2& v) { return isFinite(v.x) && isFinite(v.z); }
20
21bool isFinite(const Vec3& v) { return isFinite(v.x) && isFinite(v.y) && isFinite(v.z); }
22
23double distance(const Vec2& a, const Vec2& b) {
24 const double dx = b.x - a.x;
25 const double dz = b.z - a.z;
26 return std::sqrt(dx * dx + dz * dz);
27}
28
29double polygonArea(const std::vector<Vec2>& poly) {
30 if (poly.size() < 3) return 0.0;
31 double acc = 0.0;
32 for (std::size_t i = 0; i < poly.size(); ++i) {
33 const Vec2& a = poly[i];
34 const Vec2& b = poly[(i + 1) % poly.size()];
35 acc += a.x * b.z - b.x * a.z;
36 }
37 return 0.5 * acc;
38}
39
40bool polygonValid(const std::vector<Vec2>& poly) {
41 if (poly.size() < 3 || poly.size() > 256) return false;
42 for (const auto& p : poly)
43 if (!isFinite(p)) return false;
44 return std::abs(polygonArea(poly)) > kEps;
45}
46
47void appendBox(MeshBake& bake, const Vec3& a, const Vec3& b, double thickness, double height, const std::string& id) {
48 Vec2 dir{b.x - a.x, b.z - a.z};
49 const double len = std::sqrt(dir.x * dir.x + dir.z * dir.z);
50 if (len <= kEps || thickness <= kEps || height <= kEps) return;
51 dir.x /= len;
52 dir.z /= len;
53 const Vec2 n{-dir.z * thickness * 0.5, dir.x * thickness * 0.5};
54 const std::uint32_t base = static_cast<std::uint32_t>(bake.positions.size());
55 const Vec3 corners[8] = {
56 {a.x + n.x, a.y, a.z + n.z}, {a.x - n.x, a.y, a.z - n.z},
57 {b.x - n.x, a.y, b.z - n.z}, {b.x + n.x, a.y, b.z + n.z},
58 {a.x + n.x, a.y + height, a.z + n.z}, {a.x - n.x, a.y + height, a.z - n.z},
59 {b.x - n.x, a.y + height, b.z - n.z}, {b.x + n.x, a.y + height, b.z + n.z},
60 };
61 for (const auto& c : corners) bake.positions.push_back(c);
62 static constexpr std::uint32_t faces[12][3] = {{0, 1, 2}, {0, 2, 3}, {4, 6, 5}, {4, 7, 6}, {0, 3, 7}, {0, 7, 4},
63 {1, 5, 6}, {1, 6, 2}, {0, 4, 5}, {0, 5, 1}, {3, 2, 6}, {3, 6, 7}};
64 for (const auto& f : faces) {
65 bake.indices.push_back(base + f[0]);
66 bake.indices.push_back(base + f[1]);
67 bake.indices.push_back(base + f[2]);
68 }
69 bake.primitiveIds.push_back(id);
70}
71
72void appendSlab(MeshBake& bake, const std::vector<Vec2>& poly, double y, double thickness, const std::string& id) {
73 if (poly.size() < 3 || thickness <= kEps) return;
74 const std::uint32_t base = static_cast<std::uint32_t>(bake.positions.size());
75 for (const auto& p : poly) bake.positions.push_back(Vec3{p.x, y, p.z});
76 for (const auto& p : poly) bake.positions.push_back(Vec3{p.x, y + thickness, p.z});
77 const std::uint32_t n = static_cast<std::uint32_t>(poly.size());
78 for (std::uint32_t i = 1; i + 1 < n; ++i) {
79 bake.indices.push_back(base);
80 bake.indices.push_back(base + i);
81 bake.indices.push_back(base + i + 1);
82 bake.indices.push_back(base + n);
83 bake.indices.push_back(base + n + i + 1);
84 bake.indices.push_back(base + n + i);
85 }
86 bake.primitiveIds.push_back(id);
87}
88
89} // namespace
90
91const Node* Document::find(const std::string& id) const {
92 const auto it = nodes_.find(id);
93 return it == nodes_.end() ? nullptr : &it->second;
94}
95
96Node* Document::findMutable(const std::string& id) {
97 const auto it = nodes_.find(id);
98 return it == nodes_.end() ? nullptr : &it->second;
99}
100
101bool Document::canParent(NodeKind child, const std::string& parentId) const {
102 if (child == NodeKind::Site) return parentId.empty();
103 const Node* parent = find(parentId);
104 if (!parent) return false;
105 switch (child) {
106 case NodeKind::Building: return parent->kind == NodeKind::Site;
107 case NodeKind::Level: return parent->kind == NodeKind::Building;
108 case NodeKind::Wall:
109 case NodeKind::Slab:
110 case NodeKind::Zone:
111 case NodeKind::Item: return parent->kind == NodeKind::Level;
112 case NodeKind::Opening: return parent->kind == NodeKind::Wall;
113 case NodeKind::Site: return false;
114 }
115 return false;
116}
117
118bool Document::validateNode(const Node& node) const {
119 if (node.id.empty() || node.id.size() > 128 || node.name.size() > 256) return false;
120 if (node.kind == NodeKind::Site) {
121 if (!node.parentId.empty()) return false;
122 } else if (!canParent(node.kind, node.parentId)) {
123 return false;
124 }
125 switch (node.kind) {
126 case NodeKind::Site:
127 case NodeKind::Building: return true;
128 case NodeKind::Level: return isFinite(node.elevation) && isFinite(node.height) && node.height > kEps;
129 case NodeKind::Wall:
130 return isFinite(node.start) && isFinite(node.end) && isFinite(node.thickness) && isFinite(node.height) &&
131 node.thickness > kEps && node.height > kEps && distance(node.start, node.end) > kEps;
132 case NodeKind::Slab:
133 return polygonValid(node.polygon) && isFinite(node.slabThickness) && node.slabThickness > kEps;
134 case NodeKind::Zone: return polygonValid(node.polygon);
135 case NodeKind::Item:
136 return isFinite(node.position) && isFinite(node.yawDegrees) && node.catalogId.size() <= 256;
138 return isFinite(node.t) && node.t >= 0.0 && node.t <= 1.0 && isFinite(node.width) && node.width > kEps &&
139 isFinite(node.openingHeight) && node.openingHeight > kEps && isFinite(node.sill) && node.sill >= 0.0;
140 }
141 return false;
142}
143
144void Document::unlinkFromParent(const std::string& id) {
145 Node* node = findMutable(id);
146 if (!node || node->parentId.empty()) return;
147 if (Node* parent = findMutable(node->parentId)) {
148 parent->children.erase(std::remove(parent->children.begin(), parent->children.end(), id),
149 parent->children.end());
150 }
151}
152
153void Document::collectDescendants(const std::string& id, std::vector<std::string>& out) const {
154 const Node* node = find(id);
155 if (!node) return;
156 for (const auto& child : node->children) {
157 out.push_back(child);
158 collectDescendants(child, out);
159 }
160}
161
163 if (nodes_.find(node.id) != nodes_.end())
165 eve::DiagnosticCode::Conflict, "ArchSpace node id already exists", "id", {}, "archspace.document"));
166 if (!validateNode(node))
168 "ArchSpace node failed validation", "node", {},
169 "archspace.document"));
170 if (node.kind == NodeKind::Site) {
171 if (!rootId_.empty())
173 "ArchSpace document already has a site root",
174 "root", {}, "archspace.document"));
175 rootId_ = node.id;
176 } else {
177 Node* parent = findMutable(node.parentId);
178 if (!parent)
180 "ArchSpace parent node is missing", "parentId", {},
181 "archspace.document"));
182 parent->children.push_back(node.id);
183 }
184 nodes_.emplace(node.id, std::move(node));
185 return ok();
186}
187
189 Node* existing = findMutable(node.id);
190 if (!existing)
192 eve::DiagnosticCode::NotFound, "ArchSpace node is missing", "id", {}, "archspace.document"));
193 node.parentId = existing->parentId;
194 node.kind = existing->kind;
195 node.children = existing->children;
196 if (!validateNode(node))
198 "ArchSpace node failed validation", "node", {},
199 "archspace.document"));
200 *existing = std::move(node);
201 return ok();
202}
203
205 if (!find(id))
207 eve::DiagnosticCode::NotFound, "ArchSpace node is missing", "id", {}, "archspace.document"));
208 std::vector<std::string> order;
209 collectDescendants(id, order);
210 std::reverse(order.begin(), order.end());
211 order.push_back(id);
212 for (const auto& victim : order) {
213 unlinkFromParent(victim);
214 nodes_.erase(victim);
215 }
216 if (rootId_ == id) rootId_.clear();
217 return ok();
218}
219
221 nodes_.clear();
222 rootId_.clear();
223}
224
225eve::Result<void> Document::bootstrap(const std::string& siteId, const std::string& buildingId,
226 const std::string& levelId, double levelHeight) {
227 if (!rootId_.empty() || siteId.empty() || buildingId.empty() || levelId.empty())
230 "ArchSpace bootstrap requires an empty document and non-empty ids", {}, {}, "archspace.document"));
231 if (siteId == buildingId || siteId == levelId || buildingId == levelId)
233 "ArchSpace bootstrap ids must be unique", {}, {},
234 "archspace.document"));
235 Node site;
236 site.id = siteId;
237 site.kind = NodeKind::Site;
238 site.name = "Site";
239 Node building;
240 building.id = buildingId;
241 building.kind = NodeKind::Building;
242 building.parentId = siteId;
243 building.name = "Building";
244 Node level;
245 level.id = levelId;
246 level.kind = NodeKind::Level;
247 level.parentId = buildingId;
248 level.name = "Level 0";
249 level.height = levelHeight;
250 Document candidate;
251 auto inserted = candidate.insert(std::move(site));
252 if (!inserted.ok()) return inserted;
253 inserted = candidate.insert(std::move(building));
254 if (!inserted.ok()) return inserted;
255 inserted = candidate.insert(std::move(level));
256 if (!inserted.ok()) return inserted;
257 *this = std::move(candidate);
258 return ok();
259}
260
261eve::Result<void> Document::createRoom(const std::string& levelId, const std::string& roomId, std::string roomName,
262 std::vector<Vec2> polygon, double wallHeight, double wallThickness,
263 double slabThickness) {
264 const Node* level = find(levelId);
265 if (!level || level->kind != NodeKind::Level)
267 "ArchSpace room requires an existing level", "levelId",
268 {}, "archspace.document"));
269 if (roomId.empty() || !polygonValid(polygon) || wallHeight <= kEps || wallThickness <= kEps ||
270 slabThickness <= kEps)
272 "ArchSpace room geometry is invalid", "polygon", {},
273 "archspace.document"));
274 if (roomName.empty()) roomName = roomId;
275
276 Document candidate = *this;
277 Node zone;
278 zone.id = roomId + ".zone";
279 zone.kind = NodeKind::Zone;
280 zone.parentId = levelId;
281 zone.name = roomName;
282 zone.polygon = polygon;
283 Node slab;
284 slab.id = roomId + ".slab";
285 slab.kind = NodeKind::Slab;
286 slab.parentId = levelId;
287 slab.name = roomName + " floor";
288 slab.polygon = polygon;
289 slab.slabThickness = slabThickness;
290 auto inserted = candidate.insert(std::move(zone));
291 if (!inserted.ok()) return inserted;
292 inserted = candidate.insert(std::move(slab));
293 if (!inserted.ok()) return inserted;
294 for (std::size_t i = 0; i < polygon.size(); ++i) {
295 Node wall;
296 wall.id = roomId + ".wall." + std::to_string(i);
297 wall.kind = NodeKind::Wall;
298 wall.parentId = levelId;
299 wall.name = roomName + " wall " + std::to_string(i);
300 wall.start = polygon[i];
301 wall.end = polygon[(i + 1) % polygon.size()];
302 wall.height = wallHeight;
303 wall.thickness = wallThickness;
304 inserted = candidate.insert(std::move(wall));
305 if (!inserted.ok()) return inserted;
306 }
307 *this = std::move(candidate);
308 return ok();
309}
310
311eve::Result<void> Document::createWall(const std::string& levelId, const std::string& wallId, std::string wallName,
312 Vec2 start, Vec2 end, double height, double thickness) {
313 const Node* level = find(levelId);
314 if (!level || level->kind != NodeKind::Level)
316 "ArchSpace wall requires an existing level", "levelId",
317 {}, "archspace.document"));
318 Node wall;
319 wall.id = wallId;
320 wall.kind = NodeKind::Wall;
321 wall.parentId = levelId;
322 wall.name = wallName.empty() ? wallId : std::move(wallName);
323 wall.start = start;
324 wall.end = end;
325 wall.height = height;
326 wall.thickness = thickness;
327 return insert(std::move(wall));
328}
329
330eve::Result<void> Document::createOpening(const std::string& wallId, const std::string& openingId,
331 std::string openingName, OpeningKind kind, double t, double width,
332 double height, double sill) {
333 const Node* wall = find(wallId);
334 if (!wall || wall->kind != NodeKind::Wall)
336 "ArchSpace opening requires an existing wall",
337 "wallId", {}, "archspace.document"));
338 Node opening;
339 opening.id = openingId;
340 opening.kind = NodeKind::Opening;
341 opening.parentId = wallId;
342 opening.name = openingName.empty() ? openingId : std::move(openingName);
343 opening.openingKind = kind;
344 opening.t = t;
345 opening.width = width;
346 opening.openingHeight = height;
347 opening.sill = sill;
348 return insert(std::move(opening));
349}
350
351eve::Result<void> Document::placeItem(const std::string& levelId, const std::string& itemId, std::string itemName,
352 std::string catalogId, Vec3 position, double yawDegrees) {
353 const Node* level = find(levelId);
354 if (!level || level->kind != NodeKind::Level)
356 "ArchSpace item requires an existing level", "levelId",
357 {}, "archspace.document"));
358 Node item;
359 item.id = itemId;
360 item.kind = NodeKind::Item;
361 item.parentId = levelId;
362 item.name = itemName.empty() ? itemId : std::move(itemName);
363 item.catalogId = std::move(catalogId);
364 item.position = position;
365 item.yawDegrees = yawDegrees;
366 return insert(std::move(item));
367}
368
369std::vector<std::string> Document::diagnostics() const {
370 std::vector<std::string> out;
371 if (rootId_.empty() && !nodes_.empty()) out.push_back("document has nodes but no root site");
372 if (!rootId_.empty() && !find(rootId_)) out.push_back("root id is missing from node map");
373 std::unordered_map<std::string, std::size_t> referenced;
374 for (const auto& [id, node] : nodes_) {
375 if (!validateNode(node)) out.push_back("node failed validation: " + id);
376 for (const auto& child : node.children) {
377 ++referenced[child];
378 const Node* childNode = find(child);
379 if (!childNode)
380 out.push_back("missing child " + child + " under " + id);
381 else if (childNode->parentId != id)
382 out.push_back("child parent mismatch: " + child);
383 }
384 if (node.kind != NodeKind::Site) {
385 const Node* parent = find(node.parentId);
386 if (!parent)
387 out.push_back("missing parent for " + id);
388 else if (!canParent(node.kind, node.parentId))
389 out.push_back("illegal parent kind for " + id);
390 }
391 }
392 for (const auto& [id, node] : nodes_) {
393 if (node.kind == NodeKind::Site) continue;
394 if (referenced.find(id) == referenced.end()) out.push_back("orphaned node: " + id);
395 }
396 return out;
397}
398
400 MeshBake bake;
401
402 for (const auto& [id, node] : nodes_) {
403 if (node.kind == NodeKind::Wall) {
404 const Node* level = find(node.parentId);
405 const double y = level ? level->elevation : 0.0;
406 std::vector<const Node*> openings;
407 openings.reserve(node.children.size());
408 for (const auto& childId : node.children) {
409 const Node* child = find(childId);
410 if (child && child->kind == NodeKind::Opening) openings.push_back(child);
411 }
412 appendWallWithOpenings(bake, node, y, openings);
413 } else if (node.kind == NodeKind::Slab) {
414 const Node* level = find(node.parentId);
415 const double y = level ? level->elevation : 0.0;
416 appendSlab(bake, node.polygon, y, node.slabThickness, id);
417 } else if (node.kind == NodeKind::Item) {
418 appendCatalogItem(bake, id, node.catalogId, node.position, node.yawDegrees);
419 }
420 }
421 return bake;
422}
423
424} // namespace eve::archspace
Duration start
LogicalId itemId
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float thickness
Definition CaveMesh.cpp:83
glm::vec4 p[6]
std::uint64_t parentId
glm::vec3 n
Definition Grass.cpp:63
std::vector< float > positions
float v
std::int32_t c
std::uint32_t height
std::uint32_t width
TokenKind kind
std::array< float, 3 > position
std::int32_t parent
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
std::vector< std::int32_t > order
int level
float f
float t
const RoadNode * node
float dz
float dx
int children
Definition TreeMesh.cpp:295
V3 dir
Definition TreeMesh.cpp:150
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Authoritative architectural space document (Pascal-like node graph).
eve::Result< void > createWall(const std::string &levelId, const std::string &wallId, std::string wallName, Vec2 start, Vec2 end, double height, double thickness)
Insert a single wall under an existing level.
eve::Result< void > eraseCascade(const std::string &id)
Delete a node and every descendant. Root deletion clears the document.
Node * findMutable(const std::string &id)
Borrow a mutable node by id. @ownership Non-owning pointer into this document's node map; do not free...
eve::Result< void > placeItem(const std::string &levelId, const std::string &itemId, std::string itemName, std::string catalogId, Vec3 position, double yawDegrees=0.0)
Place a catalog item on an existing level.
eve::Result< void > bootstrap(const std::string &siteId, const std::string &buildingId, const std::string &levelId, double levelHeight=3.0)
Create site → building → level skeleton when the document is empty.
const Node * find(const std::string &id) const
Borrow a node by id. @ownership Non-owning pointer into this document's node map; do not free....
void clear()
Remove every node.
std::vector< std::string > diagnostics() const
Collect structural and geometric diagnostics as human-readable lines.
eve::Result< void > replace(Node node)
Replace authored fields while preserving id, kind, parent and children.
eve::Result< void > insert(Node node)
Insert a validated node and link it into the parent child list.
MeshBake bakeMesh() const
Build wall boxes (with opening cutouts), slabs and item markers for viewport upload.
eve::Result< void > createOpening(const std::string &wallId, const std::string &openingId, std::string openingName, OpeningKind kind, double t, double width, double height, double sill=0.0)
Insert a door/window opening on an existing wall.
eve::Result< void > createRoom(const std::string &levelId, const std::string &roomId, std::string roomName, std::vector< Vec2 > polygon, double wallHeight, double wallThickness, double slabThickness)
Create a closed room: perimeter walls + slab + zone under an existing level.
OpeningKind
Opening semantic used by doors and windows hosted on walls.
void appendWallWithOpenings(MeshBake &bake, const Node &wall, double elevation, const std::vector< const Node * > &openings)
Bake one wall with merged opening cavities and explicit reveal liners.
void appendCatalogItem(MeshBake &bake, const std::string &itemId, const std::string &catalogId, const Vec3 &position, double yawDegrees)
Append yaw-oriented catalog geometry into a mesh bake.
NodeKind
Stable node kind discriminator for the architectural document.
bool isFinite(float value)
True when finite.
Renderer-neutral triangle mesh rebuilt from the document.
One authored architectural node. Only fields for the active kind are meaningful.
std::vector< std::string > children
std::vector< Vec2 > polygon
Metres on the level XZ plane.
Metres in world/level space.