Maths.js

The complete source of src/Maths.js, exactly as it ships, in reading order. Every line is present; the file is split at its own section banners.

Maths.js - reactive mirror of the PHP `Maths` module (composite types)

// ==========================================================================
// Maths.js - reactive mirror of the PHP `Maths` module (composite types)
// ==========================================================================
//
// The Maths types the editor front end binds to. Only the composite value types
// live here; the scalar primitives Maths.Real and Maths.Integer are framework
// primitives (referenced bare as field types, the same way Text and Boolean are)
// and are not redefined.
//
// Exact 1:1 mirrors of the PHP data classes, same fields, same order:
//   Maths.Vector2     maps to  MathsVector2Data     (x, y)
//   Maths.Vector3     maps to  MathsVector3Data     (x, y, z)
//   Maths.Vector4     maps to  MathsVector4Data     (x, y, z, w)
//   Maths.Quaternion  maps to  MathsQuaternionData  (x, y, z, w)
//
// Each component is a Maths.Real (PHP float). Consumers: Clip.Marker.Data
// (position + orientation), Clip.Scene.Camera.Data, and through them
// Clip.Snapshot.Data. Bundled here in public_html/clip for now alongside the
// other reactive definitions; this moves to the framework Maths module later.
// --------------------------------------------------------------------------

Reactive.Module("Maths", () => {

  // MathsVector2Data - a 2-component real vector (uv, screen point).
  Reactive.Type("Vector2", () => {
    Reactive.Field("x", Maths.Real);
    Reactive.Field("y", Maths.Real);
  });

  // MathsVector3Data - a 3-component real vector (position, direction, colour).
  Reactive.Type("Vector3", () => {
    Reactive.Field("x", Maths.Real);
    Reactive.Field("y", Maths.Real);
    Reactive.Field("z", Maths.Real);
  });

  // MathsVector4Data - a 4-component real vector (rgba, homogeneous point).
  Reactive.Type("Vector4", () => {
    Reactive.Field("x", Maths.Real);
    Reactive.Field("y", Maths.Real);
    Reactive.Field("z", Maths.Real);
    Reactive.Field("w", Maths.Real);
  });

  // MathsQuaternionData - an orientation quaternion; identity is (0, 0, 0, 1).
  Reactive.Type("Quaternion", () => {
    Reactive.Field("x", Maths.Real);
    Reactive.Field("y", Maths.Real);
    Reactive.Field("z", Maths.Real);
    Reactive.Field("w", Maths.Real);
  });

});

Number -> Text conversions (functions hung off the scalar primitives)

// --------------------------------------------------------------------------
// Number -> Text conversions (functions hung off the scalar primitives)
// --------------------------------------------------------------------------
//
// Maths.Real and Maths.Integer are framework primitives (defined in Reactive.js).
// Reopening each as a module here MERGES a ToString function onto it without touching
// its native validator - the same way Text.js merges Text.Equal onto the Text primitive.
// ToString derives a reactive Text from a reactive number, so a numeric value can be
// shown with Content.Text (which is typed to Text). It inherits the number's lock/error
// through Reactive.Value inside the Calculate.
//
//   Maths.Integer.ToString(value)  a whole number  -> reactive Text
//   Maths.Real.ToString(value)     a real number   -> reactive Text

Reactive.Module("Maths", () => {
  Reactive.Module("Integer", () => {
    Reactive.Function("ToString", () => {
      const value = Reactive.Argument(Maths.Integer);
      return Reactive.Calculate(() => String(Reactive.Value(value)));
    });
  });
  Reactive.Module("Real", () => {
    Reactive.Function("ToString", () => {
      const value = Reactive.Argument(Maths.Real);
      return Reactive.Calculate(() => String(Reactive.Value(value)));
    });
  });
});

Arithmetic as an OVERRIDABLE INTERFACE (doc 14 sec 8)

// --------------------------------------------------------------------------
// Arithmetic as an OVERRIDABLE INTERFACE (doc 14 sec 8)
// --------------------------------------------------------------------------
//
// Maths.Add / Subtract / Multiply are interface members: calling one dispatches to the
// currently bound implementation of Maths. The default HOST implementation (Maths.Host,
// below, bound at the root) computes synchronously with Reactive.Calculate - that is
// exactly "Calculate is the synchronous host implementation of the arithmetic interface".
// Under a Reactive.Mode you can bind a different implementation (e.g. a codegen one that
// emits an operation per call and assembles source instead of computing) with no change
// to the call sites. So basic maths is driver/interface-based and overridable.

Reactive.Interface("Maths", () => {
  Reactive.Function("Add", () => {
    Reactive.Argument(Maths.Real);
    Reactive.Argument(Maths.Real);
    Reactive.Returns(Maths.Real);
  });
  Reactive.Function("Subtract", () => {
    Reactive.Argument(Maths.Real);
    Reactive.Argument(Maths.Real);
    Reactive.Returns(Maths.Real);
  });
  Reactive.Function("Multiply", () => {
    Reactive.Argument(Maths.Real);
    Reactive.Argument(Maths.Real);
    Reactive.Returns(Maths.Real);
  });
  Reactive.Function("Divide", () => {
    Reactive.Argument(Maths.Real);
    Reactive.Argument(Maths.Real);
    Reactive.Returns(Maths.Real);
  });
});

// The host (default) implementation of the Maths interface: synchronous Reactive.Calculate.
Reactive.Module("Maths", () => {
  Reactive.Module("Host", () => {
    Reactive.Function("Add", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      return Reactive.Calculate(() => Reactive.Value(a) + Reactive.Value(b));
    });
    Reactive.Function("Subtract", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      return Reactive.Calculate(() => Reactive.Value(a) - Reactive.Value(b));
    });
    Reactive.Function("Multiply", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      return Reactive.Calculate(() => Reactive.Value(a) * Reactive.Value(b));
    });
    Reactive.Function("Divide", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      return Reactive.Calculate(() => Reactive.Value(a) / Reactive.Value(b));
    });
  });
});

// The Calculate-based host is retired (Reactive.Calculate is gone). The driver-based Maths.Driver
// (defined below) is bound as the root default at the end of this file, so Maths.Add(...) emits a
// typed operation instead of computing inline. Maths.Host is kept only for reference / as a codegen
// override example; it is no longer bound and its Reactive.Calculate calls are never executed.
// Reactive.With(Maths, Maths.Host);

Arithmetic as EMIT / GATHER / DRIVER (the maths analogue of the Dom pipeline)

// --------------------------------------------------------------------------
// Arithmetic as EMIT / GATHER / DRIVER (the maths analogue of the Dom pipeline)
// --------------------------------------------------------------------------
//
// Instead of a union of every possible operation, one GENERIC operation object goes into the
// 'maths' channel (exactly as one generic node object goes into 'content'): the operation name,
// its operands, and the settable id its result goes into. Each operand is a Maths.Operand - a
// GENERIC value union with a variant per value type - so the emitted array is typed. The maths
// FUNCTIONS type-check their arguments (Reactive.Argument(Maths.Real)), so a program that calls
// them with the wrong type does not pass the Mount type-check pass. The DRIVER re-checks operand
// types as a safety net and, if they are wrong, writes the reactive ERROR that is built into every
// reactive value onto the result instead of a number. Each op's result starts LOADING and the
// driver fills it once its operands are settled, so a chain settles bottom-up over the run loop.

Reactive.Module("Maths", () => {
  // The generic value union: one variant per value type (extend as more are needed).
  Reactive.Union("Operand", () => {
    Reactive.Variant("real", Maths.Real);
    Reactive.Variant("integer", Maths.Integer);
  });
  // The generic operation object: the op name, its operands, and the settable id of its result.
  Reactive.Type("Operation", () => {
    Reactive.Field("name", Text);
    Reactive.Field("operands", Reactive.Any);
    Reactive.Field("result", Reactive.Any);
  });
});

// The DRIVER implementation of the Maths interface: each op type-checks its Real arguments, opens a
// Loading result, wraps its operands in the value union, and emits ONE generic operation. The
// arguments are already type-safe, so the emitted operands are the right type by construction.
Reactive.Module("Maths", () => {
  Reactive.Module("Driver", () => {
    Reactive.Function("Add", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      const result = Reactive.Loading(Maths.Real)(0);
      const operands = [{ real: a }, { real: b }];
      const operation = { name: "Add", operands: operands, result: result };
      Reactive.Emit(Maths.Operation)("maths", operation);
      return result;
    });
    Reactive.Function("Subtract", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      const result = Reactive.Loading(Maths.Real)(0);
      const operands = [{ real: a }, { real: b }];
      const operation = { name: "Subtract", operands: operands, result: result };
      Reactive.Emit(Maths.Operation)("maths", operation);
      return result;
    });
    Reactive.Function("Multiply", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      const result = Reactive.Loading(Maths.Real)(0);
      const operands = [{ real: a }, { real: b }];
      const operation = { name: "Multiply", operands: operands, result: result };
      Reactive.Emit(Maths.Operation)("maths", operation);
      return result;
    });
    Reactive.Function("Divide", () => {
      const a = Reactive.Argument(Maths.Real);
      const b = Reactive.Argument(Maths.Real);
      const result = Reactive.Loading(Maths.Real)(0);
      const operands = [{ real: a }, { real: b }];
      const operation = { name: "Divide", operands: operands, result: result };
      Reactive.Emit(Maths.Operation)("maths", operation);
      return result;
    });
  });
});

// The 'maths' driver: gather the operations, and for each whose operands are settled, check the
// operand types, compute, and write the result (or a reactive error) into its settable.
Reactive.Module("Maths", () => {
  const RV = Reactive._internal.ReactiveValue;
  const RS = Reactive._internal.ReactiveSettable;

  function mathsNative(value) {
    if (value != null && typeof value === "object" && value.optionalGettable !== undefined) {
      return value.optionalGettable == null ? undefined : value.optionalGettable.value;
    }
    return value;
  }

  function operandBinding(operand) {
    if (operand.real !== undefined) {
      return operand.real;
    }
    if (operand.integer !== undefined) {
      return operand.integer;
    }
    return null;
  }

  function computeOperation(name, values) {
    if (name === "Add") {
      return values[0] + values[1];
    }
    if (name === "Subtract") {
      return values[0] - values[1];
    }
    if (name === "Multiply") {
      return values[0] * values[1];
    }
    if (name === "Divide") {
      return values[0] / values[1];
    }
    return undefined;
  }

  Reactive.Driver("maths", (params, addUpdates) => {
    const lastSignature = {};   // result id -> last value/error written, so a settled op is not re-set
    let evaluateCount = 0;

    return (operations) => {
      evaluateCount++;
      const updates = {};
      for (const operation of operations) {
        const bindings = operation.operands.map(operandBinding);
        const anyLocked = bindings.some((b) => Reactive.IsLocked(b));
        if (anyLocked) {
          continue;   // an operand is still loading (an upstream op) - leave this result loading
        }
        const natives = bindings.map(mathsNative);
        const badOperand = natives.some((n) => typeof n !== "number");
        const optionalError = badOperand ? ("Maths." + operation.name + ": operand is not a number") : null;
        const value = optionalError != null ? 0 : computeOperation(operation.name, natives);
        // The operation carries the result BINDING, not an id. The driver derives the settable id
        // here (ids never appear in application data), and only a settable result can be written.
        const resultId = Reactive.Reference(operation.result);
        const signature = optionalError != null ? ("E:" + optionalError) : ("V:" + value);
        if (lastSignature[resultId] === signature) {
          continue;   // unchanged - do not schedule another run
        }
        lastSignature[resultId] = signature;
        const built = Reactive.Create(value, optionalError != null, optionalError == null ? undefined : optionalError);
        const settable = new RS(resultId, undefined);
        updates[resultId] = new RV(built.optionalGettable, settable);
      }
      console.log("[Maths] evaluate #" + evaluateCount + ":", operations.length, "operation(s),", Object.keys(updates).length, "update(s)");
      if (Object.keys(updates).length > 0) {
        addUpdates(updates);
      }
    };
  }, true);   // always active: maths is a core driver, no Mount config needed
});

// Bind the driver-based implementation as the root default (replaces the retired Calculate host).
Reactive.With(Maths, Maths.Driver);