载入中...
搜索中...
未找到
ArchSpaceTarget.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <utility>
6
8namespace {
9
10const EditorValue* field(const EditorValue& value, const char* key) {
11 const auto* object = value.getIf<EditorValue::Object>();
12 if (!object) return nullptr;
13 const auto it = object->find(key);
14 return it == object->end() ? nullptr : &it->second;
15}
16
17bool readNumber(const EditorValue* value, double& out) {
18 if (!value) return false;
19 if (const auto* real = value->getIf<double>()) {
20 out = *real;
21 return std::isfinite(out);
22 }
23 if (const auto* integer = value->getIf<std::int64_t>()) {
24 out = static_cast<double>(*integer);
25 return true;
26 }
27 return false;
28}
29
30EditorValue vec2Value(const archspace::Vec2& v) { return EditorValue::Array{v.x, v.z}; }
31
32EditorValue vec3Value(const archspace::Vec3& v) { return EditorValue::Array{v.x, v.y, v.z}; }
33
34bool parseVec2(const EditorValue& value, archspace::Vec2& out) {
35 const auto* array = value.getIf<EditorValue::Array>();
36 if (!array || array->size() != 2) return false;
37 return readNumber(&(*array)[0], out.x) && readNumber(&(*array)[1], out.z);
38}
39
40bool parseVec3(const EditorValue& value, archspace::Vec3& out) {
41 const auto* array = value.getIf<EditorValue::Array>();
42 if (!array || array->size() != 3) return false;
43 return readNumber(&(*array)[0], out.x) && readNumber(&(*array)[1], out.y) && readNumber(&(*array)[2], out.z);
44}
45
46EditorValue nodeValue(const archspace::Node& node) {
48 for (const auto& child : node.children) children.push_back(child);
49 EditorValue::Array polygon;
50 for (const auto& p : node.polygon) polygon.push_back(vec2Value(p));
52 {"id", node.id},
53 {"kind", std::string(archspace::nodeKindName(node.kind))},
54 {"parentId", node.parentId},
55 {"name", node.name},
56 {"children", std::move(children)},
57 {"elevation", node.elevation},
58 {"height", node.height},
59 {"start", vec2Value(node.start)},
60 {"end", vec2Value(node.end)},
61 {"thickness", node.thickness},
62 {"polygon", std::move(polygon)},
63 {"slabThickness", node.slabThickness},
64 {"position", vec3Value(node.position)},
65 {"yawDegrees", node.yawDegrees},
66 {"catalogId", node.catalogId},
67 {"openingKind", std::string(archspace::openingKindName(node.openingKind))},
68 {"t", node.t},
69 {"width", node.width},
70 {"openingHeight", node.openingHeight},
71 {"sill", node.sill},
72 };
73}
74
75Result<archspace::Node> parseNode(const EditorValue& value) {
76 archspace::Node node;
77 const auto* id = field(value, "id");
78 const auto* kind = field(value, "kind");
79 const auto* parentId = field(value, "parentId");
80 const auto* name = field(value, "name");
81 const auto* ids = id ? id->getIf<std::string>() : nullptr;
82 const auto* kinds = kind ? kind->getIf<std::string>() : nullptr;
83 const auto* parents = parentId ? parentId->getIf<std::string>() : nullptr;
84 const auto* names = name ? name->getIf<std::string>() : nullptr;
85 if (!ids || ids->empty() || !kinds || !parents || !names)
86 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.node"),
87 "ArchSpace node identity is invalid");
88 auto parsedKind = archspace::parseNodeKind(*kinds);
89 if (!parsedKind.ok())
90 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.kind"),
91 "ArchSpace node kind is unknown");
92 node.kind = parsedKind.value();
93 node.id = *ids;
94 node.parentId = *parents;
95 node.name = *names;
96 if (const auto* children = field(value, "children")) {
97 const auto* array = children->getIf<EditorValue::Array>();
98 if (!array)
99 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.children"),
100 "ArchSpace children must be an array");
101 for (const auto& child : *array) {
102 const auto* text = child.getIf<std::string>();
103 if (!text)
104 return eve::editing::failed<archspace::Node>(
105 EditorStatus::Rejected, RuleId("editor.archspace.children"), "ArchSpace child id must be a string");
106 node.children.push_back(*text);
107 }
108 }
109 readNumber(field(value, "elevation"), node.elevation);
110 readNumber(field(value, "height"), node.height);
111 if (const auto* start = field(value, "start"); start && !parseVec2(*start, node.start))
112 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.wall"),
113 "ArchSpace wall start is invalid");
114 if (const auto* end = field(value, "end"); end && !parseVec2(*end, node.end))
115 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.wall"),
116 "ArchSpace wall end is invalid");
117 readNumber(field(value, "thickness"), node.thickness);
118 if (const auto* polygon = field(value, "polygon")) {
119 const auto* array = polygon->getIf<EditorValue::Array>();
120 if (!array)
121 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.polygon"),
122 "ArchSpace polygon must be an array");
123 for (const auto& point : *array) {
124 archspace::Vec2 v;
125 if (!parseVec2(point, v))
126 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.polygon"),
127 "ArchSpace polygon point is invalid");
128 node.polygon.push_back(v);
129 }
130 }
131 readNumber(field(value, "slabThickness"), node.slabThickness);
132 if (const auto* position = field(value, "position"); position && !parseVec3(*position, node.position))
133 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.item"),
134 "ArchSpace item position is invalid");
135 readNumber(field(value, "yawDegrees"), node.yawDegrees);
136 if (const auto* catalog = field(value, "catalogId")) {
137 const auto* text = catalog->getIf<std::string>();
138 if (!text)
139 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.item"),
140 "ArchSpace catalog id must be a string");
141 node.catalogId = *text;
142 }
143 if (const auto* opening = field(value, "openingKind")) {
144 const auto* text = opening->getIf<std::string>();
145 if (!text)
146 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.opening"),
147 "ArchSpace opening kind must be a string");
149 if (!kind.ok())
150 return eve::editing::failed<archspace::Node>(EditorStatus::Rejected, RuleId("editor.archspace.opening"),
151 "ArchSpace opening kind is invalid");
152 node.openingKind = kind.value();
153 }
154 readNumber(field(value, "t"), node.t);
155 readNumber(field(value, "width"), node.width);
156 readNumber(field(value, "openingHeight"), node.openingHeight);
157 readNumber(field(value, "sill"), node.sill);
158 return eve::editing::applied<archspace::Node>(std::move(node));
159}
160
162 double minimum = -1e9, double maximum = 1e9) {
164 d.path = PropertyPath(std::move(path));
165 d.displayNameKey = "editor.archspace." + d.path.value();
166 d.category = category;
167 d.type = type;
168 d.defaultValue = std::move(defaultValue);
169 d.flags = PropertyFlag::EditorOnly;
170 d.numeric.minimum = minimum;
171 d.numeric.maximum = maximum;
172 return d;
173}
174
175bool hasErrors(const std::vector<EditorDiagnostic>& values) {
176 return std::any_of(values.begin(), values.end(),
177 [](const auto& d) { return d.severity() == DiagnosticSeverity::Error; });
178}
179
180} // namespace
181
183
185 return {TargetId(id_),
186 "archspace-document",
188 false,
190}
191
193 if (capability == propertyCapabilityId()) return static_cast<IPropertyProvider*>(this);
195 return static_cast<IEditingSnapshotProvider*>(this);
196 return nullptr;
197}
198
200 return EditorValue::Object{{"schemaId", std::string("eve.archspace.document")},
201 {"schemaVersion", int64_t{1}},
202 {"content", contentValue()}};
203}
204
205bool ArchSpaceDocumentTarget::matches(const SelectionSnapshot& selection) const {
206 if (selection.items.empty()) return false;
207 for (const auto& item : selection.items) {
208 if (item.target != TargetId(id_) || !document_.find(item.item.value())) return false;
209 }
210 return true;
211}
212
213const archspace::Node* ArchSpaceDocumentTarget::selectedNode(const SelectionSnapshot& selection) const {
214 if (selection.items.size() != 1 || selection.items.front().target != TargetId(id_)) return nullptr;
215 return document_.find(selection.items.front().item.value());
216}
217
219 if (!matches(selection))
221 eve::DiagnosticCode::InvalidArgument, "ArchSpace selection mismatch", "editor.archspace.selection"));
223}
224
227 const archspace::Node* node = selectedNode(selection);
228 if (!node) return schema;
229 schema.typeId = std::string("archspace.") + archspace::nodeKindName(node->kind);
230 schema.properties.push_back(prop("name", PropertyType::String, node->name, "common"));
231 switch (node->kind) {
233 schema.properties.push_back(prop("elevation", PropertyType::Float, node->elevation, "level"));
234 schema.properties.push_back(prop("height", PropertyType::Float, node->height, "level", 0.1, 1000));
235 break;
237 schema.properties.push_back(prop("thickness", PropertyType::Float, node->thickness, "wall", 0.01, 10));
238 schema.properties.push_back(prop("height", PropertyType::Float, node->height, "wall", 0.1, 1000));
239 break;
241 schema.properties.push_back(prop("catalogId", PropertyType::String, node->catalogId, "item"));
242 schema.properties.push_back(prop("yawDegrees", PropertyType::Float, node->yawDegrees, "item"));
243 break;
245 schema.properties.push_back(prop("t", PropertyType::Float, node->t, "opening", 0, 1));
246 schema.properties.push_back(prop("width", PropertyType::Float, node->width, "opening", 0.01, 20));
247 schema.properties.push_back(
248 prop("openingHeight", PropertyType::Float, node->openingHeight, "opening", 0.01, 20));
249 schema.properties.push_back(prop("sill", PropertyType::Float, node->sill, "opening", 0, 20));
250 break;
251 default: break;
252 }
253 return schema;
254}
255
257 const archspace::Node* node = selectedNode(selection);
258 if (!node || !schema(selection).find(path)) return {};
259 if (path == PropertyPath("name")) return {PropertyReadState::Value, node->name, {}};
260 if (path == PropertyPath("elevation")) return {PropertyReadState::Value, node->elevation, {}};
261 if (path == PropertyPath("height")) return {PropertyReadState::Value, node->height, {}};
262 if (path == PropertyPath("thickness")) return {PropertyReadState::Value, node->thickness, {}};
263 if (path == PropertyPath("catalogId")) return {PropertyReadState::Value, node->catalogId, {}};
264 if (path == PropertyPath("yawDegrees")) return {PropertyReadState::Value, node->yawDegrees, {}};
265 if (path == PropertyPath("t")) return {PropertyReadState::Value, node->t, {}};
266 if (path == PropertyPath("width")) return {PropertyReadState::Value, node->width, {}};
267 if (path == PropertyPath("openingHeight")) return {PropertyReadState::Value, node->openingHeight, {}};
268 if (path == PropertyPath("sill")) return {PropertyReadState::Value, node->sill, {}};
269 return {};
270}
271
272EditorValue ArchSpaceDocumentTarget::contentValue() const {
274 for (const auto& [id, node] : document_.nodes()) {
275 (void)id;
276 nodes.push_back(nodeValue(node));
277 }
278 return EditorValue::Object{{"rootId", document_.rootId()}, {"nodes", std::move(nodes)}};
279}
280
281Result<DomainOperation> ArchSpaceDocumentTarget::replacement(EditorValue content, std::string property) const {
283 op.type = "archspace.document.replace.v1";
284 op.inverseType = op.type;
285 op.target = TargetId(id_);
286 op.payload = std::move(content);
287 op.inverse = contentValue();
288 op.hasInverse = true;
289 if (!property.empty()) op.affectedProperties.push_back(property);
290 op.mergeKey = "archspace:" + id_ + ":" + (property.empty() ? "structure" : property);
291 return eve::editing::applied<DomainOperation>(std::move(op));
292}
293
294Result<void> ArchSpaceDocumentTarget::installContent(const EditorValue& content) {
295 const auto* rootIdValue = field(content, "rootId");
296 const auto* nodesValue = field(content, "nodes");
297 const auto* rootId = rootIdValue ? rootIdValue->getIf<std::string>() : nullptr;
298 const auto* nodes = nodesValue ? nodesValue->getIf<EditorValue::Array>() : nullptr;
299 if (!rootId || !nodes || !content.isWithinLimits(12, 250000, 32 * 1024 * 1024))
300 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.payload"),
301 "ArchSpace document payload is invalid or exceeds limits");
302 if (nodes->size() > 100000)
303 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.budget"),
304 "ArchSpace document exceeds 100000 nodes");
305
306 archspace::Document candidate;
307 if (rootId->empty() && nodes->empty()) {
308 document_ = std::move(candidate);
309 return eve::editing::applied<void>();
310 }
311 // Insert sites first, then remaining nodes in dependency-friendly passes.
312 std::vector<archspace::Node> parsed;
313 parsed.reserve(nodes->size());
314 for (const auto& value : *nodes) {
315 auto node = parseNode(value);
316 if (!node.ok())
317 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.node"),
318 "ArchSpace document contains an invalid node");
319 parsed.push_back(std::move(node.value()));
320 }
321 std::sort(parsed.begin(), parsed.end(), [](const archspace::Node& a, const archspace::Node& b) {
322 return static_cast<int>(a.kind) < static_cast<int>(b.kind);
323 });
324 for (auto& node : parsed) {
325 node.children.clear(); // re-linked by insert()
326 auto inserted = candidate.insert(std::move(node));
327 if (!inserted.ok())
328 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.insert"),
329 "ArchSpace document failed hierarchical insert");
330 }
331 if (candidate.rootId() != *rootId)
332 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.root"),
333 "ArchSpace root id does not match document content");
334 if (!candidate.diagnostics().empty())
335 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.invalid"),
336 "ArchSpace document validation failed");
337 document_ = std::move(candidate);
338 return eve::editing::applied<void>();
339}
340
341std::vector<EditorDiagnostic> ArchSpaceDocumentTarget::validate() const {
342 std::vector<EditorDiagnostic> out;
343 for (const auto& line : document_.diagnostics()) {
345 RuleId("editor.archspace.validate"), DiagnosticSeverity::Error,
346 line));
347 }
348 if (document_.nodeCount() > 100000) {
350 RuleId("editor.archspace.budget"), DiagnosticSeverity::Error,
351 "ArchSpace document exceeds 100000 nodes"));
352 }
353 return out;
354}
355
357 const PropertyPath& path, const EditorValue& value,
358 PropertySetMode mode) const {
359 if (mode == PropertySetMode::Reset) return makeReset(selection, path);
360 const auto descriptor = schema(selection).find(path);
361 const archspace::Node* node = selectedNode(selection);
362 if (!descriptor || !node || mode != PropertySetMode::Absolute)
363 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.set"),
364 "ArchSpace property requires a matching absolute edit");
365 auto checked = validatePropertyValue(*descriptor, value);
366 if (!checked.ok())
367 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.value"),
368 "ArchSpace property value is invalid");
369 archspace::Document candidate = document_;
370 archspace::Node* editable = candidate.findMutable(node->id);
371 if (!editable)
372 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.archspace.node"),
373 "ArchSpace node is missing");
374 if (path == PropertyPath("name")) {
375 const auto* text = value.getIf<std::string>();
376 if (!text)
377 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.name"),
378 "Name must be string");
379 editable->name = *text;
380 } else if (path == PropertyPath("catalogId")) {
381 const auto* text = value.getIf<std::string>();
382 if (!text)
383 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.catalog"),
384 "Catalog id must be string");
385 editable->catalogId = *text;
386 } else {
387 double number = 0.0;
388 if (!readNumber(&value, number))
389 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.number"),
390 "ArchSpace property requires a number");
391 if (path == PropertyPath("elevation"))
392 editable->elevation = number;
393 else if (path == PropertyPath("height"))
394 editable->height = number;
395 else if (path == PropertyPath("thickness"))
396 editable->thickness = number;
397 else if (path == PropertyPath("yawDegrees"))
398 editable->yawDegrees = number;
399 else if (path == PropertyPath("t"))
400 editable->t = number;
401 else if (path == PropertyPath("width"))
402 editable->width = number;
403 else if (path == PropertyPath("openingHeight"))
404 editable->openingHeight = number;
405 else if (path == PropertyPath("sill"))
406 editable->sill = number;
407 else
408 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.archspace.property"),
409 "Unsupported ArchSpace property");
410 }
411 auto replaced = candidate.replace(*editable);
412 if (!replaced.ok())
413 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.invalid"),
414 "ArchSpace property update failed validation");
415 ArchSpaceDocumentTarget staged(id_);
416 staged.document_ = std::move(candidate);
417 if (hasErrors(staged.validate()))
418 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.invalid"),
419 "ArchSpace property update failed validation");
420 return replacement(staged.contentValue(), path.value());
421}
422
424 const PropertyPath& path) const {
425 const auto descriptor = schema(selection).find(path);
426 if (!descriptor)
427 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.archspace.property"),
428 "Unknown ArchSpace property");
429 return makeSet(selection, path, descriptor->defaultValue, PropertySetMode::Absolute);
430}
431
433 const std::string& buildingId,
434 const std::string& levelId,
435 double levelHeight) const {
436 archspace::Document candidate = document_;
437 auto boot = candidate.bootstrap(siteId, buildingId, levelId, levelHeight);
438 if (!boot.ok())
439 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.bootstrap"),
440 "ArchSpace bootstrap requires an empty document and unique ids");
441 ArchSpaceDocumentTarget staged(id_);
442 staged.document_ = std::move(candidate);
443 return replacement(staged.contentValue());
444}
445
447 const std::string& levelId, const std::string& roomId, std::string roomName, double originX, double originZ,
448 double sizeX, double sizeZ, double wallHeight, double wallThickness, double slabThickness) const {
449 if (!(std::isfinite(originX) && std::isfinite(originZ) && std::isfinite(sizeX) && std::isfinite(sizeZ)) ||
450 sizeX <= 1e-6 || sizeZ <= 1e-6)
451 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.room"),
452 "ArchSpace room size must be finite and positive");
453 std::vector<archspace::Vec2> polygon = {
454 {originX, originZ},
455 {originX + sizeX, originZ},
456 {originX + sizeX, originZ + sizeZ},
457 {originX, originZ + sizeZ},
458 };
459 archspace::Document candidate = document_;
460 auto created = candidate.createRoom(levelId, roomId, std::move(roomName), std::move(polygon), wallHeight,
461 wallThickness, slabThickness);
462 if (!created.ok())
463 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.room"),
464 "ArchSpace room could not be created under the given level");
465 ArchSpaceDocumentTarget staged(id_);
466 staged.document_ = std::move(candidate);
467 return replacement(staged.contentValue());
468}
469
471 const std::string& wallId, std::string wallName,
473 double height, double thickness) const {
474 archspace::Document candidate = document_;
475 auto created = candidate.createWall(levelId, wallId, std::move(wallName), start, end, height, thickness);
476 if (!created.ok())
477 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.wall"),
478 "ArchSpace wall could not be created under the given level");
479 ArchSpaceDocumentTarget staged(id_);
480 staged.document_ = std::move(candidate);
481 return replacement(staged.contentValue());
482}
483
485 const std::string& openingId,
487 double width, double height,
488 double sill) const {
489 archspace::Document candidate = document_;
490 auto created = candidate.createOpening(wallId, openingId, openingId, kind, t, width, height, sill);
491 if (!created.ok())
492 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.opening"),
493 "ArchSpace opening insert failed validation");
494 ArchSpaceDocumentTarget staged(id_);
495 staged.document_ = std::move(candidate);
496 return replacement(staged.contentValue());
497}
498
500 const std::string& itemId, std::string catalogId,
502 double yawDegrees) const {
503 archspace::Document candidate = document_;
504 auto placed =
505 candidate.placeItem(levelId, itemId, itemId, std::move(catalogId), position, yawDegrees);
506 if (!placed.ok())
507 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.archspace.item"),
508 "ArchSpace item insert failed validation");
509 ArchSpaceDocumentTarget staged(id_);
510 staged.document_ = std::move(candidate);
511 return replacement(staged.contentValue());
512}
513
515 archspace::Document candidate = document_;
516 auto erased = candidate.eraseCascade(id.value());
517 if (!erased.ok())
518 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.archspace.delete"),
519 "ArchSpace node was not found");
520 ArchSpaceDocumentTarget staged(id_);
521 staged.document_ = std::move(candidate);
522 return replacement(staged.contentValue());
523}
524
526 if (operation.target != TargetId(id_) || operation.type != "archspace.document.replace.v1")
527 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.operation"),
528 "ArchSpace operation mismatch");
529 ArchSpaceDocumentTarget candidate(id_);
530 auto installed = candidate.installContent(operation.payload);
531 if (!installed.ok()) return installed;
532 if (hasErrors(candidate.validate()))
533 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.archspace.invalid"),
534 "ArchSpace document validation failed");
535 document_ = std::move(candidate.document_);
536 bumpRevision();
537 widenDirty(0, 0);
538 return eve::editing::applied<void>();
539}
540
541std::unique_ptr<IDomainOperationTarget> ArchSpaceDocumentTarget::cloneDomainState() const {
542 return std::make_unique<ArchSpaceDocumentTarget>(*this);
543}
544
545Result<void> ArchSpaceDocumentTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
546 auto* typed = dynamic_cast<ArchSpaceDocumentTarget*>(candidate.get());
547 if (!typed || typed->id_ != id_)
548 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.archspace.candidate"),
549 "ArchSpace candidate mismatch");
550 *this = *typed;
551 return eve::editing::applied<void>();
552}
553
555 const auto* schemaIdValue = field(snapshot, "schemaId");
556 const auto* versionValue = field(snapshot, "schemaVersion");
557 const auto* content = field(snapshot, "content");
558 const auto* schemaId = schemaIdValue ? schemaIdValue->getIf<std::string>() : nullptr;
559 const auto* version = versionValue ? versionValue->getIf<std::int64_t>() : nullptr;
560 if (!schemaId || *schemaId != "eve.archspace.document" || !version || *version != 1 || !content)
561 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.archspace.snapshot"),
562 "Unsupported ArchSpace snapshot");
564 op.target = TargetId(id_);
565 op.type = "archspace.document.replace.v1";
566 op.payload = *content;
567 auto result = applyDomainOperation(op);
568 if (result.ok()) clearDirtyRegion();
569 return result;
570}
571
573 EditorGizmoSnapshot snapshot;
574 snapshot.status = EditorStatus::Applied;
575 snapshot.target = id_;
576 snapshot.targetRevision = revisionValue();
577 for (const auto& [id, node] : document_.nodes()) {
578 (void)id;
579 if (node.kind == archspace::NodeKind::Wall) {
580 EditorGizmoPrimitive primitive;
581 primitive.id = node.id;
582 primitive.kind = "segment";
583 primitive.position = {node.start.x,
584 document_.find(node.parentId) ? document_.find(node.parentId)->elevation : 0.0,
585 node.start.z};
586 primitive.direction = {node.end.x - node.start.x, 0.0, node.end.z - node.start.z};
587 primitive.length = std::sqrt(primitive.direction[0] * primitive.direction[0] +
588 primitive.direction[2] * primitive.direction[2]);
589 primitive.color = {0.2, 0.7, 1.0, 0.9};
590 snapshot.primitives.push_back(std::move(primitive));
591 } else if (node.kind == archspace::NodeKind::Zone && !node.polygon.empty()) {
592 double minX = node.polygon.front().x, maxX = minX, minZ = node.polygon.front().z, maxZ = minZ;
593 for (const auto& p : node.polygon) {
594 minX = std::min(minX, p.x);
595 maxX = std::max(maxX, p.x);
596 minZ = std::min(minZ, p.z);
597 maxZ = std::max(maxZ, p.z);
598 }
599 EditorGizmoPrimitive primitive;
600 primitive.id = node.id;
601 primitive.kind = "area";
602 primitive.dashed = true;
603 primitive.position = {(minX + maxX) * 0.5, 0.02, (minZ + maxZ) * 0.5};
604 primitive.size = {maxX - minX, 0.01, maxZ - minZ};
605 primitive.color = {0.8, 0.2, 1.0, 0.25};
606 snapshot.primitives.push_back(std::move(primitive));
607 } else if (node.kind == archspace::NodeKind::Item) {
608 EditorGizmoPrimitive primitive;
609 primitive.id = node.id;
610 primitive.kind = "point";
611 primitive.position = {node.position.x, node.position.y, node.position.z};
612 primitive.radius = 0.15;
613 primitive.color = {1.0, 0.8, 0.2, 0.9};
614 primitive.text = node.catalogId;
615 snapshot.primitives.push_back(std::move(primitive));
616 } else if (node.kind == archspace::NodeKind::Opening) {
617 const archspace::Node* wall = document_.find(node.parentId);
618 if (!wall) continue;
619 EditorGizmoPrimitive primitive;
620 primitive.id = node.id;
621 primitive.kind = "point";
622 primitive.position = {wall->start.x + (wall->end.x - wall->start.x) * node.t,
623 node.sill + node.openingHeight * 0.5,
624 wall->start.z + (wall->end.z - wall->start.z) * node.t};
625 primitive.radius = 0.12;
626 primitive.color = {0.2, 1.0, 0.4, 0.9};
627 primitive.text = archspace::openingKindName(node.openingKind);
628 snapshot.primitives.push_back(std::move(primitive));
629 }
630 }
631 return eve::editing::applied<EditorGizmoSnapshot>(std::move(snapshot));
632}
633
634} // namespace eve::archspace_editing
ActionParameterOperation operation
double value
Duration start
LogicalId itemId
std::string descriptor
float thickness
Definition CaveMesh.cpp:83
glm::vec4 p[6]
std::map< std::string, Var > values
std::uint64_t parentId
float maximum[3]
float minimum[3]
std::uint32_t key
glm::uvec4 ids
float v
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
std::array< float, 3 > position
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< BvhNode > nodes
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float d
float t
const RoadNode * node
double number
int created
bool placed
int children
Definition TreeMesh.cpp:295
const char * category
glm::vec3 point
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
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
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....
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.
const std::unordered_map< std::string, Node > & nodes() const noexcept
const std::string & rootId() const noexcept
Root id.
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.
std::size_t nodeCount() const noexcept
Node count.
Revisioned ArchSpace document target for Inspector, tools and automation.
EditorValue snapshotValue() const override
Snapshot value.
Result< DomainOperation > makeBootstrap(const std::string &siteId, const std::string &buildingId, const std::string &levelId, double levelHeight=3.0) const
Plan site→building→level creation on an empty document.
ArchSpaceDocumentTarget(std::string id)
Arch space document target.
Result< DomainOperation > makeCreateWall(const std::string &levelId, const std::string &wallId, std::string wallName, archspace::Vec2 start, archspace::Vec2 end, double height=3.0, double thickness=0.2) const
Plan a single wall under a level.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Result< DomainOperation > makeDeleteNode(const ObjectId &id) const
Plan cascading deletion of one node.
TargetDescriptor describe() const override
Describe.
static CapabilityId propertyCapabilityId()
Capability identity published by describe() for Inspector property editing.
Result< void > loadSnapshot(const EditorValue &snapshot)
Loads snapshot.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
eve::Result< eve::Revision > currentRevision(const SelectionSnapshot &selection) const override
Current revision.
void * queryCapability(const CapabilityId &capability) override
Query Inspector property capability. @ownership Borrowed from this target; callers must not delete it...
PropertyReadResult read(const SelectionSnapshot &selection, const PropertyPath &path) const override
Reads .
Result< DomainOperation > makeCreateOpening(const std::string &wallId, const std::string &openingId, archspace::OpeningKind kind, double t, double width, double height, double sill=0.0) const
Plan a door/window opening on an existing wall.
Result< DomainOperation > makeSet(const SelectionSnapshot &selection, const PropertyPath &path, const EditorValue &value, PropertySetMode mode) const override
Make set.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
std::vector< EditorDiagnostic > validate() const
Validate.
Result< DomainOperation > makeReset(const SelectionSnapshot &selection, const PropertyPath &path) const override
Make reset.
Result< DomainOperation > makePlaceItem(const std::string &levelId, const std::string &itemId, std::string catalogId, archspace::Vec3 position, double yawDegrees=0.0) const
Plan a catalog item placement on a level.
PropertySchema schema(const SelectionSnapshot &selection) const override
Schema.
Result< EditorGizmoSnapshot > gizmo() const
Build a revision-bound overlay for walls, zones, items and openings.
Result< DomainOperation > makeCreateRectRoom(const std::string &levelId, const std::string &roomId, std::string roomName, double originX, double originZ, double sizeX, double sizeZ, double wallHeight=3.0, double wallThickness=0.2, double slabThickness=0.2) const
Plan a rectangular room (four walls + slab + zone) under a level.
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
virtual void clearDirtyRegion()=0
Clears dirty region.
Optional capability exposing a deterministic, persistence-safe editing snapshot.
static CapabilityId editingCapabilityId()
Stable capability identity shared by every editable domain target.
UI-independent property access for an immutable selection snapshot.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
const char * defaultValue
editing::PropertyDescriptor PropertyDescriptor
editing::DomainOperation DomainOperation
editing::Value EditorValue
editing::PropertyPath PropertyPath
editing::TargetId TargetId
editing::PropertyType PropertyType
editing::SelectionSnapshot SelectionSnapshot
OpeningKind
Opening semantic used by doors and windows hosted on walls.
eve::Result< NodeKind > parseNodeKind(const std::string &text) noexcept
Parse a node kind wire name.
const char * openingKindName(OpeningKind kind) noexcept
Stable wire name for an opening kind. @ownership Non-owning pointer to a static string literal; do no...
const char * nodeKindName(NodeKind kind) noexcept
Stable wire name for a node kind. @ownership Non-owning pointer to a static string literal; do not fr...
eve::Result< OpeningKind > parseOpeningKind(const std::string &text) noexcept
Parse an opening kind wire name.
EVENGINE_API_PLATFORM bool hasErrors(const std::vector< DnutDiagnostic > &diagnostics)
Return whether any diagnostic is an error.
PropertySetMode
PropertySetMode public API.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
const EditorValue * field(const EditorValue &value, const char *name)
bool readNumber(const EditorValue &value, const char *name, float &output)
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
One authored architectural node. Only fields for the active kind are meaningful.
Metres on the level XZ plane.
Metres in world/level space.
DomainOperation public API.
Renderer-neutral wire primitive for authoring viewport overlays.
std::array< double, 3 > size
std::array< double, 3 > direction
std::array< double, 3 > position
std::array< double, 4 > color
Immutable revision-tagged overlay snapshot consumed by any viewport renderer.
std::vector< GizmoPrimitive > primitives
PropertyReadResult public API.
EVENGINE_API_PLATFORM public API.
std::vector< PropertyDescriptor > properties
PropertyDescriptorLookup find(const PropertyPath &path) const
Finds find.
SelectionSnapshot public API.
std::vector< SelectionItem > items
Stable discovery metadata exposed by every editable target.