20constexpr float kPi = 3.14159265358979323846f;
24 : cols_(
cols), rows_(
rows), spacing_(
spacing), originX_(originX), originY_(originY) {
25 if (cols_ < 2 || rows_ < 2)
26 throw Exception(
"Cloth: cols and rows must be >= 2");
28 throw Exception(
"Cloth: spacing must be > 0");
30 particles_.resize(
static_cast<size_t>(cols_ * rows_));
31 for (
int r = 0;
r < rows_; ++
r) {
32 for (
int c = 0;
c < cols_; ++
c) {
33 Particle &
p = particles_[
static_cast<size_t>(
r * cols_ +
c)];
34 p.x =
p.px = originX_ + float(
c) * spacing_;
35 p.y =
p.py = originY_ + float(
r) * spacing_;
48 if (cols_ < 2 || rows_ < 2)
return;
50 particles_.resize(
static_cast<size_t>(cols_ * rows_));
51 for (
int r = 0;
r < rows_; ++
r) {
52 for (
int c = 0;
c < cols_; ++
c) {
53 Particle &
p = particles_[
static_cast<size_t>(
r * cols_ +
c)];
54 p.x =
p.px = originX_ + float(
c) * spacing_;
55 p.y =
p.py = originY_ + float(
r) * spacing_;
63 forceX_ = forceY_ = 0.f;
64 interactStrength_ = 0.f;
67void Cloth::rebuildLinks() {
69 auto add = [&](
int a,
int b) {
74 const Particle &pa = particles_[
static_cast<size_t>(
a)];
75 const Particle &pb = particles_[
static_cast<size_t>(
b)];
76 const float dx = pb.x - pa.x;
77 const float dy = pb.y - pa.y;
78 link.rest = std::sqrt(
dx *
dx +
dy *
dy);
79 if (link.rest > 1e-4f) links_.push_back(link);
82 for (
int r = 0;
r < rows_; ++
r) {
83 for (
int c = 0;
c < cols_; ++
c) {
84 const int i =
r * cols_ +
c;
86 if (
c + 1 < cols_) add(i, i + 1);
87 if (
r + 1 < rows_) add(i, i + cols_);
89 if (
c + 1 < cols_ &&
r + 1 < rows_) add(i, i + cols_ + 1);
90 if (
c > 0 &&
r + 1 < rows_) add(i, i + cols_ - 1);
92 if (
c + 2 < cols_) add(i, i + 2);
93 if (
r + 2 < rows_) add(i, i + cols_ * 2);
99void Cloth::buildLinkKeys() {
101 for (
const Link &link : links_) {
102 const int a = link.a;
103 const int b = link.b;
104 const int lo = std::min(
a,
b);
105 const int hi = std::max(
a,
b);
106 linkKeys_.insert((int64_t(lo) << 32) | int64_t(hi));
110bool Cloth::areLinked(
int a,
int b)
const {
111 if (
a ==
b)
return true;
112 const int lo = std::min(
a,
b);
113 const int hi = std::max(
a,
b);
114 return linkKeys_.find((int64_t(lo) << 32) | int64_t(hi)) != linkKeys_.end();
117bool Cloth::validIndex(
int index)
const {
127 stiffness_ = std::clamp(stiffness, 0.f, 1.f);
135 damping_ = std::clamp(damping, 0.f, 1.f);
139 particleSize_ = std::max(1.f,
size);
143 particleMass_ = std::max(1e-4f, mass);
149 foldStiffness_ = std::clamp(k, 0.f, 1.f);
153 maxFoldAngle_ = std::clamp(
degrees, 0.f, 180.f) * kPi / 180.f;
157 if (
w <= 0.f ||
h <= 0.f) {
171 if (!validIndex(
index))
throw Exception(
"Cloth.pin: index out of range");
172 particles_[
static_cast<size_t>(
index)].
pinned =
true;
176 if (!validIndex(
index))
throw Exception(
"Cloth.unpin: index out of range");
177 particles_[
static_cast<size_t>(
index)].
pinned =
false;
181 for (
int c = 0;
c < cols_; ++
c)
182 particles_[
static_cast<size_t>(
c)].pinned =
true;
186 if (!validIndex(
index))
return false;
187 return particles_[
static_cast<size_t>(
index)].
pinned;
194 const Particle &
p = particles_[
static_cast<size_t>(i)];
195 if (
p.pinned)
continue;
196 const float dx =
p.x -
x;
197 const float dy =
p.y -
y;
211 if (!validIndex(grabIndex_))
return;
212 Particle &
p = particles_[
static_cast<size_t>(grabIndex_)];
229 interactRadius_ = std::max(0.f,
radius);
235int64_t Cloth::cellKey(
int cx,
int cy)
const {
236 return (int64_t(uint32_t(
cx)) << 32) | int64_t(uint32_t(
cy));
239void Cloth::rebuildHash() {
241 const float cell = std::max(1e-3f, particleSize_ * 2.f);
242 const float inv = 1.f /
cell;
244 const Particle &
p = particles_[
static_cast<size_t>(i)];
245 const int cx = int(std::floor(
p.x * inv));
246 const int cy = int(std::floor(
p.y * inv));
247 hash_[cellKey(
cx,
cy)].push_back(i);
251void Cloth::solveSelfCollision() {
253 const float minDist = particleSize_ * 2.f;
254 if (minDist <= 0.f)
return;
256 const float cell = std::max(1e-3f, minDist);
257 const float inv = 1.f /
cell;
259 Particle &pi = particles_[
static_cast<size_t>(i)];
260 const int cx = int(std::floor(pi.x * inv));
261 const int cy = int(std::floor(pi.y * inv));
262 for (
int oy = -1;
oy <= 1; ++
oy) {
263 for (
int ox = -1;
ox <= 1; ++
ox) {
264 auto it = hash_.find(cellKey(
cx +
ox,
cy +
oy));
265 if (it == hash_.end())
continue;
266 for (
int j : it->
second) {
267 if (j <= i)
continue;
268 if (areLinked(i, j))
continue;
269 Particle &pj = particles_[
static_cast<size_t>(j)];
270 float dx = pj.x - pi.x;
271 float dy = pj.y - pi.y;
273 if (d2 >= minDist * minDist || d2 < 1e-8f)
continue;
274 const float d = std::sqrt(d2);
277 const float corr = std::min(0.5f, (minDist -
d) /
d);
278 if (pi.pinned && pj.pinned)
continue;
282 if (pi.pinned && !pj.pinned) { wa = 0.f; wb = 1.f; }
283 else if (pj.pinned && !pi.pinned) { wa = 1.f; wb = 0.f; }
284 pi.x -=
dx * corr * wa;
285 pi.y -=
dy * corr * wa;
286 pj.x +=
dx * corr * wb;
287 pj.y +=
dy * corr * wb;
294void Cloth::solveFoldConstraint() {
295 if (foldStiffness_ <= 0.f || maxFoldAngle_ >= kPi)
return;
297 const float maxDev = maxFoldAngle_;
298 const auto straighten = [&](
int mid,
int a,
int b) {
303 Particle &pm = particles_[
static_cast<size_t>(
mid)];
304 if (pm.pinned)
return;
305 const Particle &pa = particles_[
static_cast<size_t>(
a)];
306 const Particle &pb = particles_[
static_cast<size_t>(
b)];
307 float v1x = pa.x - pm.x, v1y = pa.y - pm.y;
308 float v2x = pb.x - pm.x, v2y = pb.y - pm.y;
309 const float l1 = std::sqrt(v1x * v1x + v1y * v1y);
310 const float l2 = std::sqrt(v2x * v2x + v2y * v2y);
311 if (l1 < 1e-6f || l2 < 1e-6f)
return;
312 v1x /= l1; v1y /= l1;
313 v2x /= l2; v2y /= l2;
314 const float dot = std::clamp(v1x * v2x + v1y * v2y, -1.f, 1.f);
315 const float angle = std::acos(dot);
316 const float dev = kPi -
angle;
317 if (dev <= maxDev)
return;
320 const float mx = (pa.x + pb.x) * 0.5f;
321 const float my = (pa.y + pb.y) * 0.5f;
324 for (
int it = 0; it < 10; ++it) {
325 const float t = (lo + hi) * 0.5f;
326 const float qx = pm.x + (mx - pm.x) *
t;
327 const float qy = pm.y + (my - pm.y) *
t;
328 const float d1x = pa.x -
qx, d1y = pa.y -
qy;
329 const float d2x = pb.x -
qx, d2y = pb.y -
qy;
330 const float s1 = std::sqrt(d1x * d1x + d1y * d1y);
331 const float s2 = std::sqrt(d2x * d2x + d2y * d2y);
332 if (s1 < 1e-6f || s2 < 1e-6f)
break;
333 const float c = std::clamp((d1x * d2x + d1y * d2y) / (s1 * s2), -1.f, 1.f);
334 const float devT = kPi - std::acos(
c);
340 const float f = hi * foldStiffness_;
341 pm.x += (mx - pm.x) *
f;
342 pm.y += (my - pm.y) *
f;
344 for (
int r = 0;
r < rows_; ++
r) {
345 for (
int c = 1;
c + 1 < cols_; ++
c) {
346 const int mid =
r * cols_ +
c;
350 for (
int c = 0;
c < cols_; ++
c) {
351 for (
int r = 1;
r + 1 < rows_; ++
r) {
352 const int mid =
r * cols_ +
c;
353 straighten(
mid,
mid - cols_,
mid + cols_);
358void Cloth::collideWorld(
float dt) {
359 if (!world_ || !world_->
isValid() || particleSize_ <= 0.f)
return;
360 const float invDt = dt > 1e-6f ? 1.f / dt : 0.f;
361 World::ClothContact contact;
363 Particle &
p = particles_[
static_cast<size_t>(i)];
364 if (
p.pinned)
continue;
365 if (!world_->
pointProbe(
p.x,
p.y, particleSize_, &contact) || !contact.hit)
continue;
367 const float vpx = (
p.x -
p.px) * invDt;
368 const float vpy = (
p.y -
p.py) * invDt;
369 p.x += contact.nx * contact.depth;
370 p.y += contact.ny * contact.depth;
377 float bodyMass = 0.f;
379 contact.body !=
nullptr && contact.body->getType() ==
"dynamic";
381 vbx = contact.body->getLinearVelocityX();
382 vby = contact.body->getLinearVelocityY();
383 bodyMass = contact.body->getMass();
385 const float vn = (vpx - vbx) * contact.nx + (vpy - vby) * contact.ny;
388 const float m = particleMass_;
389 const float reduced = bodyMass > 0.f ? (
m * bodyMass) / (
m + bodyMass) :
m;
392 const float maxKick = 900.f;
393 j = std::min(j, maxKick *
m);
394 const float kick = (j /
m) * dt;
395 p.x += contact.nx * kick;
396 p.y += contact.ny * kick;
397 if (dynamic && bodyMass > 0.f) {
398 contact.body->applyLinearImpulse(-contact.nx * j, -contact.ny * j);
412 if (!validIndex(
index))
return 0.f;
413 return particles_[
static_cast<size_t>(
index)].
x;
417 if (!validIndex(
index))
return 0.f;
418 return particles_[
static_cast<size_t>(
index)].
y;
422 if (!validIndex(
index))
throw Exception(
"Cloth.setParticlePosition: index out of range");
423 Particle &
p = particles_[
static_cast<size_t>(
index)];
428void Cloth::integrate(
float dt) {
429 if (dt <= 0.f)
return;
430 const float ax = gravityX_ + forceX_;
431 const float ay = gravityY_ + forceY_;
432 const float damp = 1.f - damping_;
433 const bool hasInteract = interactRadius_ > 0.f && interactStrength_ != 0.f;
435 for (Particle &
p : particles_) {
441 const float vx = (
p.x -
p.px) * damp;
442 const float vy = (
p.y -
p.py) * damp;
445 p.x +=
vx +
ax * dt * dt;
446 p.y +=
vy +
ay * dt * dt;
448 const float dx = interactX_ -
p.x;
449 const float dy = interactY_ -
p.y;
451 const float R2 = interactRadius_ * interactRadius_;
452 if (r2 < R2 && r2 > 1e-6f) {
453 const float r = std::sqrt(r2);
454 const float w = 1.f -
r / interactRadius_;
455 p.x += (
dx /
r) * interactStrength_ *
w * dt * dt;
456 p.y += (
dy /
r) * interactStrength_ *
w * dt * dt;
461 constexpr float maxSpeed = 900.f;
462 const float maxDisp = maxSpeed * dt;
463 const float dvx =
p.x -
p.px;
464 const float dvy =
p.y -
p.py;
465 const float v2 = dvx * dvx + dvy * dvy;
466 if (v2 > maxDisp * maxDisp) {
467 const float s = maxDisp / std::sqrt(v2);
468 p.px =
p.x - dvx *
s;
469 p.py =
p.y - dvy *
s;
474void Cloth::solveConstraints() {
475 for (
int iter = 0; iter < iterations_; ++iter) {
476 for (
const Link &link : links_) {
477 Particle &
a = particles_[
static_cast<size_t>(link.a)];
478 Particle &
b = particles_[
static_cast<size_t>(link.b)];
479 if (
a.pinned &&
b.pinned)
continue;
480 float dx =
b.x -
a.x;
481 float dy =
b.y -
a.y;
482 float dist = std::sqrt(
dx *
dx +
dy *
dy);
483 if (dist < 1e-5f)
continue;
484 const float diff = (dist - link.rest) / dist * stiffness_;
489 }
else if (
b.pinned) {
493 const float half = diff * 0.5f;
500 if (validIndex(grabIndex_)) {
501 Particle &
g = particles_[
static_cast<size_t>(grabIndex_)];
510void Cloth::collideBounds() {
511 if (!hasBounds_)
return;
512 const float minX = boundX_;
513 const float minY = boundY_;
514 const float maxX = boundX_ + boundW_;
515 const float maxY = boundY_ + boundH_;
516 constexpr float bounce = 0.35f;
518 for (Particle &
p : particles_) {
519 if (
p.pinned)
continue;
521 const float vx =
p.x -
p.px;
524 }
else if (
p.x > maxX) {
525 const float vx =
p.x -
p.px;
530 const float vy =
p.y -
p.py;
533 }
else if (
p.y > maxY) {
534 const float vy =
p.y -
p.py;
542 if (destroyed_)
return;
543 if (dt < 0.f) dt = 0.f;
544 if (dt > 0.05f) dt = 0.05f;
546 updateSubsteps(dt, 2);
549void Cloth::updateSubsteps(
float dt,
int substeps) {
550 if (destroyed_ || substeps < 1)
return;
553 const float h = dt / float(substeps);
554 for (
int s = 0;
s < substeps; ++
s) {
557 solveFoldConstraint();
558 solveSelfCollision();
561 if (grabIndex_ >= 0) {
562 Particle &
p = particles_[
static_cast<size_t>(grabIndex_)];
570 interactStrength_ = 0.f;
577 auto valid = detail::validateSimulationStep(stepValue,
settings, observation_);
579 auto next = detail::advanceSimulationObservation(observation_, stepValue);
583 }
catch (
const std::exception &
error) {
590 observation_ = std::move(next).takeValue();
595 auto valid = detail::validateSimulationObservation(
observation,
"physics.cloth.restoreObservation");
599 "Cannot restore a destroyed cloth",
600 "physics.cloth.restoreObservation"));
606 if (!gfx || destroyed_)
return;
607 const Color linkColor(colorR_, colorG_, colorB_, colorA_ * 0.75f);
608 const Color nodeColor(colorR_, colorG_, colorB_, colorA_);
610 for (
const Link &link : links_) {
611 const Particle &
a = particles_[
static_cast<size_t>(link.a)];
612 const Particle &
b = particles_[
static_cast<size_t>(link.b)];
613 const float x1 =
a.x, y1 =
a.y, x2 =
b.x, y2 =
b.y;
614 const float dx = x2 - x1;
615 const float dy = y2 - y1;
616 const float len = std::sqrt(
dx *
dx +
dy *
dy);
617 const int steps = std::max(1,
int(len / 4.f));
618 for (
int i = 0; i <=
steps; ++i) {
619 const float t = float(i) / float(
steps);
623 for (
const Particle &
p : particles_) {
624 const float s =
p.pinned ? std::max(5.f, particleSize_ + 2.f) : particleSize_;
627 if (grabIndex_ >= 0) {
628 const Particle &
p = particles_[
static_cast<size_t>(grabIndex_)];
630 Color(1.f, 0.85f, 0.35f, 0.9f));
std::map< Cell, int > best
TerrainThermalSettings settings
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.
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
EVENGINE_API_FOUNDATION public API.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
virtual void drawSolidRect(float x, float y, float w, float h, float r, float g, float b, float a=1.f)
RGBA-float overload matching the script-facing drawSolidRect name.
void setParticlePosition(int index, float x, float y)
Sets the particle position.
void setMaxFoldAngle(float degrees)
Maximum bend deviation from a straight row/column segment in degrees Range is 0..180 degrees; default...
eve::Result< void > restoreObservation(const SimulationObservation &observation) override
Restores tick/progress metadata after an owner-level restore.
void setBounds(float x, float y, float w, float h)
Axis-aligned walls; particles bounce inside. Disabled if w/h <= 0.
void setParticleSize(float size)
Particle radius used for self-collision, draw size and body collision Default is 3....
void setStiffness(float stiffness)
Constraint relaxation strength in [0,1] (default 0.85).
void clearBounds()
Clears bounds.
void setDamping(float damping)
Damping applied to Verlet velocity [0,1] (default 0.01).
float getParticleX(int index) const
Returns the particle x.
void reset()
Restore the flat grid pose (top row pinned) and clear transient state.
void interactAt(float x, float y, float radius, float strength)
Pointer-field interaction like Fluid2D::interactAt: positive strength attracts, negative repels....
void setParticleMass(float mass)
Implicit particle mass in kg (default 0.1). Used for mass-proportional momentum exchange when collidi...
bool isPinned(int index) const
True when pinned.
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings) override
Advances cloth with the shared ticked backend contract.
float getParticleY(int index) const
Returns the particle y.
void setGravity(float gx, float gy)
Sets the gravity.
int getParticleCount() const
Returns the particle count.
void moveGrab(float x, float y)
Moves grab.
void update(float dt)
Updates .
void setColor(float r, float g, float b, float a=1.f)
Sets the color.
Cloth(int cols, int rows, float spacing, float originX, float originY)
Cloth.
void setCollideWorld(World *world)
Attach a 2D World so free particles collide with its non-sensor fixtures in pixel space....
void setSelfCollision(bool on)
Enable proximity-based self-collision between non-adjacent particles Default is true.
void applyForce(float fx, float fy)
Applies force.
void draw(graphics::Graphics *gfx)
Draws .
SimulationObservation observation() const noexcept override
Returns completed tick/time observables.
void setIterations(int iterations)
Constraint solver iterations per substep (default 4).
int grabAt(float x, float y, float radius=24.f)
Grab nearest free particle within radius (pixels). Returns particle index, or -1 if none.
void releaseGrab()
Release grab.
void setFoldStiffness(float k)
Strength of the fold-angle clamp [0,1] (default 0.5).
void unpin(int index)
Unpin.
void pinTopRow()
Pin top row.
Box2D world wrapper (2D physics) with pixel-space coordinates. Handles stepping, gravity,...
bool pointProbe(float x, float y, float radius, ClothContact *out) const
Probe the deepest non-sensor fixture within radius of a pixel point. Returns false when nothing is hi...
bool isValid() const
True while the underlying Box2D world is alive.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Optional physics backend for vehicle mobility and body attach.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
glm::vec4 Color
Render-neutral RGBA color shared by graphics-facing modules.
One deterministic fixed-step emitted by SimulationClock.
Duration delta
Fixed simulation duration for this step.
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.