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);