feat: define moving source trajectories
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
function copyPosition(position) {
|
||||
if (!Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite)) {
|
||||
throw new TypeError("source position must be a finite [x, y, z] array.");
|
||||
}
|
||||
return [position[0], position[1], position[2]];
|
||||
}
|
||||
|
||||
function requireFiniteTime(time, label = "time") {
|
||||
if (!Number.isFinite(time)) throw new TypeError(label + " must be finite.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a source position at an absolute time measured from the beginning of
|
||||
* the scene. Valid trajectories are ordered by the scene contract. Values
|
||||
* outside their covered interval hold the nearest endpoint position.
|
||||
*/
|
||||
export function sourcePositionAtTime(source, time) {
|
||||
requireFiniteTime(time);
|
||||
const trajectory = source?.trajectory;
|
||||
if (!Array.isArray(trajectory) || trajectory.length === 0) {
|
||||
return copyPosition(source?.position);
|
||||
}
|
||||
|
||||
const first = trajectory[0];
|
||||
const last = trajectory[trajectory.length - 1];
|
||||
if (time <= first.time) return copyPosition(first.position);
|
||||
if (time >= last.time) return copyPosition(last.position);
|
||||
|
||||
let low = 0;
|
||||
let high = trajectory.length - 1;
|
||||
while (high - low > 1) {
|
||||
const middle = (low + high) >> 1;
|
||||
if (trajectory[middle].time <= time) low = middle;
|
||||
else high = middle;
|
||||
}
|
||||
|
||||
const left = trajectory[low];
|
||||
const right = trajectory[high];
|
||||
const ratio = (time - left.time) / (right.time - left.time);
|
||||
return left.position.map((coordinate, index) =>
|
||||
coordinate + (right.position[index] - coordinate) * ratio,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new shallow scene/source graph whose source positions are resolved
|
||||
* at one instant. Trajectories stay attached so callers can sample the returned
|
||||
* scene again; the input scene and source objects are never mutated.
|
||||
*/
|
||||
export function sceneAtTime(scene, time) {
|
||||
requireFiniteTime(time);
|
||||
if (!scene || !Array.isArray(scene.sources)) {
|
||||
throw new TypeError("scene.sources must be an array.");
|
||||
}
|
||||
return {
|
||||
...scene,
|
||||
sources: scene.sources.map((source) => ({
|
||||
...source,
|
||||
position: sourcePositionAtTime(source, time),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate a moving scene as stationary during [startTime, endTime] by
|
||||
* sampling every trajectory at the window midpoint.
|
||||
*/
|
||||
export function sampleQuasiStaticWindow(scene, startTime, endTime) {
|
||||
requireFiniteTime(startTime, "startTime");
|
||||
requireFiniteTime(endTime, "endTime");
|
||||
if (startTime < 0 || endTime <= startTime) {
|
||||
throw new RangeError("window must satisfy 0 <= startTime < endTime.");
|
||||
}
|
||||
if (Number.isFinite(scene?.duration) && endTime > scene.duration) {
|
||||
throw new RangeError("window endTime must not exceed scene.duration.");
|
||||
}
|
||||
const sampleTime = startTime + (endTime - startTime) / 2;
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
sampleTime,
|
||||
duration: endTime - startTime,
|
||||
scene: sceneAtTime(scene, sampleTime),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a scene interval into quasi-static windows. The final window is
|
||||
* shortened to the requested end time instead of being dropped.
|
||||
*/
|
||||
export function createQuasiStaticWindows(scene, options = {}) {
|
||||
const startTime = options.startTime ?? 0;
|
||||
const endTime = options.endTime ?? scene?.duration;
|
||||
const windowDuration = options.windowDuration;
|
||||
const hopDuration = options.hopDuration ?? windowDuration;
|
||||
requireFiniteTime(startTime, "startTime");
|
||||
requireFiniteTime(endTime, "endTime");
|
||||
requireFiniteTime(windowDuration, "windowDuration");
|
||||
requireFiniteTime(hopDuration, "hopDuration");
|
||||
if (startTime < 0 || endTime <= startTime) {
|
||||
throw new RangeError("interval must satisfy 0 <= startTime < endTime.");
|
||||
}
|
||||
if (windowDuration <= 0 || hopDuration <= 0) {
|
||||
throw new RangeError("windowDuration and hopDuration must be positive.");
|
||||
}
|
||||
if (Number.isFinite(scene?.duration) && endTime > scene.duration) {
|
||||
throw new RangeError("endTime must not exceed scene.duration.");
|
||||
}
|
||||
|
||||
const windows = [];
|
||||
for (let windowStart = startTime; windowStart < endTime; windowStart += hopDuration) {
|
||||
const windowEnd = Math.min(windowStart + windowDuration, endTime);
|
||||
windows.push(sampleQuasiStaticWindow(scene, windowStart, windowEnd));
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
Reference in New Issue
Block a user