14 DiagnosticSeverity::Error, std::move(
message)));
19 if (!
object)
return nullptr;
20 const auto found =
object->find(
key);
21 return found ==
object->end() ? nullptr : &
found->second;
31 result.
pins.push_back({GraphPinId(
id.
value() +
".in"),
id,
"particle.flow",
32 GraphPinDirection::Input});
34 result.
pins.push_back({GraphPinId(
id.
value() +
".out"),
id,
"particle.flow",
35 GraphPinDirection::Output});
41 const auto* typed =
value ?
value->getIf<
double>() :
nullptr;
42 return typed && std::isfinite(*typed) && *typed >=
minimum && *typed <=
maximum;
47 const auto* typed =
value ?
value->getIf<int64_t>() :
nullptr;
57 std::initializer_list<const char*>
choices) {
59 const auto* typed =
value ?
value->getIf<std::string>() :
nullptr;
60 if (!typed)
return false;
61 return std::any_of(
choices.begin(),
choices.end(), [&](
const char* item) { return *typed == item; });
65 bool nonNegative =
false) {
68 if (!typed || typed->size() !=
size)
return false;
69 return std::all_of(typed->begin(), typed->end(), [&](
const EditorValue& component) {
70 const auto* item = component.getIf<double>();
71 return item && std::isfinite(*item) && (!nonNegative || *item >= 0.0);
80 result.
allowed =
from.direction == GraphPinDirection::Output &&
81 to.direction == GraphPinDirection::Input &&
from.type ==
"particle.flow" &&
82 to.type ==
"particle.flow" &&
from.node !=
to.node;
86 DiagnosticSeverity::Error,
"Particle graph requires flow output-to-input between distinct nodes"));
91 const std::string&
type)
const {
93 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId(
"editor.particles.node-id"),
94 "Particle node id is required");
96 if (
type ==
"emission") {
105 return eve::editing::applied<GraphNodeRecord>(
node(
id,
type, std::move(
properties),
false,
true));
107 if (
type ==
"motion") {
114 }
else if (
type ==
"collision") {
120 }
else if (
type ==
"renderer") {
130 }
else if (
type ==
"output") {
136 return eve::editing::applied<GraphNodeRecord>(
node(
id,
type, std::move(
properties),
true,
false));
138 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Unsupported, RuleId(
"editor.particles.node-type"),
139 "Unknown particle node type: " +
type);
148 result.
status = EditorStatus::Rejected;
149 error(result,
"editor.particles.wrong-domain",
"Graph is not a particle emitter document");
152 if (
graph.schemaVersion != 1) {
153 result.
status = EditorStatus::Unsupported;
154 error(result,
"editor.particles.unsupported-schema",
"Particle graph schema version is unsupported");
157 std::map<GraphNodeId, const GraphNodeRecord*>
nodes;
158 std::map<GraphPinId, GraphNodeId> pins;
159 std::map<GraphPinId, const GraphPinRecord*> pinRecords;
160 int emissionCount = 0;
161 int rendererCount = 0;
164 if (!
nodes.emplace(item.id, &item).second)
165 error(result,
"editor.particles.duplicate-node",
"Duplicate particle node id: " + item.id.value());
167 pins.emplace(pin.id, item.id);
168 pinRecords.emplace(pin.id, &pin);
170 emissionCount += item.type ==
"emission";
171 rendererCount += item.type ==
"renderer";
172 outputCount += item.type ==
"output";
174 if (emissionCount != 1 || rendererCount != 1 || outputCount != 1)
175 error(result,
"editor.particles.required-modules",
176 "Particle graph requires exactly one emission, renderer and output node");
177 std::map<GraphNodeId, GraphNodeId> next;
178 std::map<GraphNodeId, int> incoming;
180 if (!pins.contains(
edge.from) || !pins.contains(
edge.to)) {
181 error(result,
"editor.particles.dangling-edge",
"Particle edge references a missing pin");
185 error(result,
"editor.particles.edge-direction",
"Particle edge has an invalid direction or type");
188 const GraphNodeId
from = pins[
edge.from];
189 const GraphNodeId
to = pins[
edge.to];
190 if (!next.emplace(
from,
to).second || ++incoming[
to] > 1)
191 error(result,
"editor.particles.branching",
"Particle module chain cannot branch or merge");
193 const auto emission = std::find_if(
nodes.begin(),
nodes.end(), [](
const auto& entry) {
194 return entry.second->type ==
"emission";
196 std::set<GraphNodeId> visited;
197 if (emission !=
nodes.end()) {
198 GraphNodeId
current = emission->first;
200 if (visited.size() !=
nodes.size() || (next.contains(
current) && visited.contains(next[
current])))
201 error(result,
"editor.particles.chain",
"Particle modules must form one acyclic emission-to-output chain");
203 error(result,
"editor.particles.output-terminal",
"Particle module chain must terminate at output");
207 for (
const auto& [
id, item] :
nodes) {
210 if (item->type ==
"emission") {
211 if (!
number(
p,
"rate", 0.0, 10000000.0) || !tuple(
p,
"lifetime", 2,
true) ||
212 !choice(
p,
"area", {
"none",
"ellipse",
"rect"}) || !tuple(
p,
"areaSize", 2,
true) ||
213 !integer(
p,
"randomSeed", -2147483648LL, 2147483647LL) ||
215 !
number(
p,
"emitterLife", -1.0, 86400.0))
216 error(result,
"editor.particles.emission-properties",
"Emission properties are invalid");
217 config[
"emission"] =
p;
218 }
else if (item->type ==
"motion") {
219 if (!
number(
p,
"direction", -360000.0, 360000.0) || !
number(
p,
"spread", 0.0, 360.0) ||
220 !tuple(
p,
"speed", 2,
true) || !tuple(
p,
"gravity", 2) ||
221 !
number(
p,
"damping", 0.0, 1.0) ||
222 !choice(
p,
"simulationSpace", {
"world",
"local"}))
223 error(result,
"editor.particles.motion-properties",
"Motion properties are invalid");
224 config[
"motion"] =
p;
225 }
else if (item->type ==
"collision") {
226 if (!choice(
p,
"mode", {
"none",
"kill",
"bounce",
"stop"}) ||
227 !
number(
p,
"radius", 0.0, 1000000.0) ||
228 !
number(
p,
"restitution", 0.0, 1.0) ||
229 !
number(
p,
"lifetimeLoss", 0.0, 1.0) || !
boolean(
p,
"world"))
230 error(result,
"editor.particles.collision-properties",
"Collision properties are invalid");
231 config[
"collision"] =
p;
232 }
else if (item->type ==
"renderer") {
233 if (!choice(
p,
"mode", {
"billboard",
"axis",
"stretched",
"ribbon"}) ||
234 !choice(
p,
"material", {
"unlit",
"lit",
"distortion"}) ||
235 !field(
p,
"texture") || !field(
p,
"texture")->getIf<std::string>() ||
236 !tuple(
p,
"size", 2,
true) || !choice(
p,
"sort", {
"none",
"oldest",
"youngest",
"distance"}) ||
237 !
boolean(
p,
"gpu") || !
boolean(
p,
"softParticles") ||
238 !
number(
p,
"softDepth", 0.0, 1.0) || !
number(
p,
"softFade", 0.0, 1.0))
239 error(result,
"editor.particles.renderer-properties",
"Renderer properties are invalid");
240 config[
"renderer"] =
p;
241 }
else if (item->type ==
"output") {
242 if (!integer(
p,
"bufferSize", 1, 10000000) || !integer(
p,
"priority", -100000, 100000) ||
243 !integer(
p,
"minimumQuality", 0, 3) || !integer(
p,
"maxSpawnPerFrame", 0, 10000000) ||
244 !choice(
p,
"overflow", {
"drop",
"pause",
"warn"}))
245 error(result,
"editor.particles.output-properties",
"Output/budget properties are invalid");
246 config[
"output"] =
p;
248 error(result,
"editor.particles.unknown-node",
"Unknown particle module: " + item->type);
252 result.
status = EditorStatus::Failed;
255 result.
status = EditorStatus::Applied;
261 double fixedStep,
int particleBudget,
262 int spawnBudgetPerFrame)
const {
268 if (compiled.status != EditorStatus::Applied || !std::isfinite(seconds) ||
269 !std::isfinite(fixedStep) || seconds < 0.0 || seconds > 600.0 || fixedStep <= 0.0 ||
270 seconds / fixedStep > 1000000.0 ||
271 particleBudget < 1 || spawnBudgetPerFrame < 0) {
272 result.
status = EditorStatus::Rejected;
273 if (compiled.status == EditorStatus::Applied)
276 DiagnosticSeverity::Error,
"Preview time, step, and budgets are invalid"));
279 const EditorValue* emission = field(compiled.configuration,
"emission");
281 const double rate = *field(*emission,
"rate")->
getIf<
double>();
283 const double maxLifetime = *
lifetime[1].getIf<
double>();
284 const int bufferSize =
static_cast<int>(*field(*
output,
"bufferSize")->
getIf<int64_t>());
285 const int configuredFrameCap =
static_cast<int>(*field(*
output,
"maxSpawnPerFrame")->
getIf<int64_t>());
286 const int frames =
static_cast<int>(std::ceil(seconds / fixedStep));
287 const int requested =
static_cast<int>(std::floor(
rate * seconds));
288 int frameCap = configuredFrameCap;
289 if (spawnBudgetPerFrame > 0 && (frameCap == 0 || spawnBudgetPerFrame < frameCap))
290 frameCap = spawnBudgetPerFrame;
291 const int admittedByFrames = frameCap > 0 ? std::min(requested, frames * frameCap) : requested;
295 static_cast<int>(std::ceil(
rate * maxLifetime))});
299 DiagnosticSeverity::Warning,
300 "Preview dropped spawns because a particle or per-frame budget was exceeded"));
304 DiagnosticSeverity::Warning,
"Preview reaches the emitter buffer capacity"));
305 result.
status = EditorStatus::Applied;
HexCoordinates to
Cell the unit walks towards on this segment.
std::map< std::string, PropertyRule > properties
std::set< std::string > choices
std::map< std::string, std::vector< std::string > > graph
std::weak_ptr< const void > lifetime
Move-only operation result carrying either a value or Status.
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.
ParticleGraphPreviewResult preview(const GraphDocumentData &graph, double seconds, double fixedStep=1.0/60.0, int particleBudget=100000, int spawnBudgetPerFrame=0) const
Estimate a deterministic preview and enforce particle/frame budgets.
GraphConnectionDecision canConnect(const GraphPinRecord &from, const GraphPinRecord &to) const override
Can connect.
ParticleGraphCompileResult compile(const GraphDocumentData &graph) const
Compile one acyclic module chain into a runtime-neutral configuration object.
Result< GraphNodeRecord > makeNode(const GraphNodeId &id, const std::string &type) const
Construct a node of type emission, motion, collision, renderer, or output.
std::string domain() const override
Domain.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
editing::Value EditorValue
Result of domain-specific connection validation.
std::vector< Diagnostic > diagnostics
Immutable graph document value sent to a domain compiler.
Directed connection between an output and input pin.
Generic graph node independent of its domain.
std::vector< GraphPinRecord > pins
Stable typed pin owned by a graph node.
Validated particle configuration compiled from a module graph.
std::vector< EditorDiagnostic > diagnostics
Revision documentRevision
EditorValue configuration
Deterministic, revision-tagged particle preview estimate.
std::vector< EditorDiagnostic > diagnostics
Revision documentRevision