Zum Inhalt

viewer/js/collision/editor.js

// editor.js — pure brush + undo state machine over a flat voxel-density grid.
// No DOM, no PlayCanvas. Callers (collision-mode.js) feed it world-space hit
// points; the editor mutates `density` in place and records voxel diffs.

const UNDO_LIMIT = 8;

export class CollisionEditor {
  constructor({ density, resolution, bounds, iso }) {
    this.density = density;
    this.resolution = resolution;
    this.bounds = bounds;
    this.iso = iso;
    this._undoRing = [];
    this._currentStroke = null;
  }

  beginStroke(kind) {
    if (kind !== 'add' && kind !== 'remove') throw new Error(`bad stroke: ${kind}`);
    this._currentStroke = { kind, indices: [], deltas: [] };
  }

  /**
   * Apply the current brush at a world-space point. `radius` is world-space;
   * `strength` is the density addend at the brush centre. Falloff is
   * quadratic: weight = 1 - (d/r)².
   */
  applyAt(point, radius, strength) {
    if (!this._currentStroke) throw new Error('no active stroke');
    const r = this.resolution;
    const sx = (this.bounds.max.x - this.bounds.min.x) / r;
    const sy = (this.bounds.max.y - this.bounds.min.y) / r;
    const sz = (this.bounds.max.z - this.bounds.min.z) / r;
    const cx = (point[0] - this.bounds.min.x) / sx;
    const cy = (point[1] - this.bounds.min.y) / sy;
    const cz = (point[2] - this.bounds.min.z) / sz;
    const ri = Math.ceil(radius / sx);
    const rj = Math.ceil(radius / sy);
    const rk = Math.ceil(radius / sz);
    const sign = this._currentStroke.kind === 'add' ? 1 : -1;
    const stroke = this._currentStroke;

    for (let k = Math.max(0, Math.floor(cz - rk)); k <= Math.min(r - 1, Math.ceil(cz + rk)); k++) {
      for (let j = Math.max(0, Math.floor(cy - rj)); j <= Math.min(r - 1, Math.ceil(cy + rj)); j++) {
        for (let i = Math.max(0, Math.floor(cx - ri)); i <= Math.min(r - 1, Math.ceil(cx + ri)); i++) {
          const wx = this.bounds.min.x + (i + 0.5) * sx;
          const wy = this.bounds.min.y + (j + 0.5) * sy;
          const wz = this.bounds.min.z + (k + 0.5) * sz;
          const dx = wx - point[0], dy = wy - point[1], dz = wz - point[2];
          const d = Math.hypot(dx, dy, dz);
          if (d > radius) continue;
          const w = 1 - (d / radius) ** 2;
          const idx = k * r * r + j * r + i;
          const before = this.density[idx];
          let after = before + sign * strength * w;
          if (after < 0) after = 0;
          const delta = after - before;
          if (delta === 0) continue;
          this.density[idx] = after;
          stroke.indices.push(idx);
          stroke.deltas.push(delta);
        }
      }
    }
  }

  endStroke() {
    if (!this._currentStroke) return;
    if (this._currentStroke.indices.length > 0) {
      this._undoRing.push({
        indices: Int32Array.from(this._currentStroke.indices),
        deltas: Float32Array.from(this._currentStroke.deltas),
      });
      while (this._undoRing.length > UNDO_LIMIT) this._undoRing.shift();
    }
    this._currentStroke = null;
  }

  undo() {
    const stroke = this._undoRing.pop();
    if (!stroke) return false;
    for (let i = 0; i < stroke.indices.length; i++) {
      const idx = stroke.indices[i];
      this.density[idx] -= stroke.deltas[i];
      if (this.density[idx] < 0) this.density[idx] = 0;
    }
    return true;
  }

  setIso(iso) { this.iso = iso; }
  undoDepth() { return this._undoRing.length; }
}