Add resolveRuntimeGlobalTimeState function and enhance runtime project time logic
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { createDefaultProjectTimeSettings } from "../document/project-time-settings";
|
||||
import {
|
||||
createDefaultProjectTimeSettings,
|
||||
type ProjectTimeSettings
|
||||
} from "../document/project-time-settings";
|
||||
|
||||
import {
|
||||
createRuntimeClockState,
|
||||
resolveRuntimeTimeState,
|
||||
type RuntimeResolvedTimeState,
|
||||
type RuntimeClockState
|
||||
} from "./runtime-project-time";
|
||||
|
||||
@@ -33,3 +38,10 @@ export function createDefaultRuntimeGlobalState(
|
||||
clock: createRuntimeClockState(timeSettings)
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveRuntimeGlobalTimeState(
|
||||
state: RuntimeGlobalState,
|
||||
timeSettings: ProjectTimeSettings
|
||||
): RuntimeResolvedTimeState {
|
||||
return resolveRuntimeTimeState(timeSettings, state.clock);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
createRuntimeClockState,
|
||||
reconfigureRuntimeClockState,
|
||||
resolveRuntimeDayNightWorldState,
|
||||
resolveRuntimeTimeState,
|
||||
type RuntimeClockState
|
||||
} from "./runtime-project-time";
|
||||
import { ThirdPersonNavigationController } from "./third-person-navigation-controller";
|
||||
@@ -722,10 +723,16 @@ export class RuntimeHost {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedTime =
|
||||
this.currentClockState === null
|
||||
? null
|
||||
: resolveRuntimeTimeState(this.runtimeScene.time, this.currentClockState);
|
||||
|
||||
const resolvedWorld = resolveRuntimeDayNightWorldState(
|
||||
this.currentWorld,
|
||||
this.runtimeScene.time,
|
||||
this.currentClockState
|
||||
this.currentClockState,
|
||||
resolvedTime
|
||||
);
|
||||
const backgroundTexture =
|
||||
resolvedWorld.background.mode === "image"
|
||||
|
||||
@@ -32,6 +32,16 @@ export interface RuntimeClockState {
|
||||
dayLengthMinutes: number;
|
||||
}
|
||||
|
||||
export type RuntimeDayPhase = "night" | "dawn" | "day" | "dusk";
|
||||
|
||||
export interface RuntimeResolvedTimeState {
|
||||
timeOfDayHours: number;
|
||||
dayCount: number;
|
||||
dayLengthMinutes: number;
|
||||
dayPhase: RuntimeDayPhase;
|
||||
isNight: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeDayNightWorldState {
|
||||
ambientLight: WorldAmbientLightSettings;
|
||||
sunLight: WorldSunLightSettings;
|
||||
@@ -52,6 +62,16 @@ interface RuntimeDayNightPhaseWeights {
|
||||
night: number;
|
||||
}
|
||||
|
||||
interface RuntimeDayPhaseWindowBoundaries {
|
||||
dawnStart: number;
|
||||
dawnEnd: number;
|
||||
duskStart: number;
|
||||
duskEnd: number;
|
||||
}
|
||||
|
||||
const MIN_RUNTIME_TIME_WINDOW_HOURS = 0.001;
|
||||
const TIME_WINDOW_EPSILON = 1e-6;
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -230,19 +250,171 @@ function wrapTimeForward(hours: number, originHours: number): number {
|
||||
return wrappedHours;
|
||||
}
|
||||
|
||||
function areTimesEquivalent(leftHours: number, rightHours: number): boolean {
|
||||
return (
|
||||
Math.abs(
|
||||
normalizeTimeOfDayHours(leftHours) - normalizeTimeOfDayHours(rightHours)
|
||||
) <= TIME_WINDOW_EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeDayPhaseWindowBoundaries(
|
||||
settings: ProjectTimeSettings
|
||||
): RuntimeDayPhaseWindowBoundaries {
|
||||
const dawnHalfDuration =
|
||||
Math.max(settings.dawnDurationHours, MIN_RUNTIME_TIME_WINDOW_HOURS) / 2;
|
||||
const duskHalfDuration =
|
||||
Math.max(settings.duskDurationHours, MIN_RUNTIME_TIME_WINDOW_HOURS) / 2;
|
||||
|
||||
return {
|
||||
dawnStart: settings.sunriseTimeOfDayHours - dawnHalfDuration,
|
||||
dawnEnd: settings.sunriseTimeOfDayHours + dawnHalfDuration,
|
||||
duskStart: settings.sunsetTimeOfDayHours - duskHalfDuration,
|
||||
duskEnd: settings.sunsetTimeOfDayHours + duskHalfDuration
|
||||
};
|
||||
}
|
||||
|
||||
function hasTimeBoundaryBeenCrossed(
|
||||
previousTimeOfDayHours: number,
|
||||
currentTimeOfDayHours: number,
|
||||
boundaryTimeOfDayHours: number
|
||||
): boolean {
|
||||
const normalizedPreviousTime = normalizeTimeOfDayHours(previousTimeOfDayHours);
|
||||
|
||||
if (areTimesEquivalent(normalizedPreviousTime, boundaryTimeOfDayHours)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wrappedCurrentTime = wrapTimeForward(
|
||||
currentTimeOfDayHours,
|
||||
normalizedPreviousTime
|
||||
);
|
||||
const wrappedBoundaryTime = wrapTimeForward(
|
||||
boundaryTimeOfDayHours,
|
||||
normalizedPreviousTime
|
||||
);
|
||||
|
||||
return (
|
||||
wrappedBoundaryTime - normalizedPreviousTime > TIME_WINDOW_EPSILON &&
|
||||
wrappedBoundaryTime <= wrappedCurrentTime + TIME_WINDOW_EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
export function isWithinTimeWindow(
|
||||
startTimeOfDayHours: number,
|
||||
endTimeOfDayHours: number,
|
||||
timeOfDayHours: number
|
||||
): boolean {
|
||||
const normalizedStartTime = normalizeTimeOfDayHours(startTimeOfDayHours);
|
||||
const normalizedEndTime = normalizeTimeOfDayHours(endTimeOfDayHours);
|
||||
const normalizedTime = normalizeTimeOfDayHours(timeOfDayHours);
|
||||
|
||||
if (areTimesEquivalent(normalizedStartTime, normalizedEndTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedStartTime < normalizedEndTime) {
|
||||
return (
|
||||
normalizedTime >= normalizedStartTime &&
|
||||
normalizedTime < normalizedEndTime
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedTime >= normalizedStartTime || normalizedTime < normalizedEndTime
|
||||
);
|
||||
}
|
||||
|
||||
export function hasTimeWindowJustStarted(
|
||||
previousTimeOfDayHours: number,
|
||||
currentTimeOfDayHours: number,
|
||||
startTimeOfDayHours: number,
|
||||
endTimeOfDayHours: number
|
||||
): boolean {
|
||||
if (areTimesEquivalent(startTimeOfDayHours, endTimeOfDayHours)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasTimeBoundaryBeenCrossed(
|
||||
previousTimeOfDayHours,
|
||||
currentTimeOfDayHours,
|
||||
startTimeOfDayHours
|
||||
);
|
||||
}
|
||||
|
||||
export function hasTimeWindowJustEnded(
|
||||
previousTimeOfDayHours: number,
|
||||
currentTimeOfDayHours: number,
|
||||
startTimeOfDayHours: number,
|
||||
endTimeOfDayHours: number
|
||||
): boolean {
|
||||
if (areTimesEquivalent(startTimeOfDayHours, endTimeOfDayHours)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasTimeBoundaryBeenCrossed(
|
||||
previousTimeOfDayHours,
|
||||
currentTimeOfDayHours,
|
||||
endTimeOfDayHours
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRuntimeDayPhase(
|
||||
settings: ProjectTimeSettings,
|
||||
timeOfDayHours: number
|
||||
): RuntimeDayPhase {
|
||||
const normalizedTime = normalizeTimeOfDayHours(timeOfDayHours);
|
||||
const boundaries = resolveRuntimeDayPhaseWindowBoundaries(settings);
|
||||
|
||||
if (
|
||||
isWithinTimeWindow(boundaries.dawnStart, boundaries.dawnEnd, normalizedTime)
|
||||
) {
|
||||
return "dawn";
|
||||
}
|
||||
|
||||
if (
|
||||
isWithinTimeWindow(boundaries.duskStart, boundaries.duskEnd, normalizedTime)
|
||||
) {
|
||||
return "dusk";
|
||||
}
|
||||
|
||||
if (
|
||||
isWithinTimeWindow(boundaries.dawnEnd, boundaries.duskStart, normalizedTime)
|
||||
) {
|
||||
return "day";
|
||||
}
|
||||
|
||||
return "night";
|
||||
}
|
||||
|
||||
export function resolveRuntimeTimeState(
|
||||
settings: ProjectTimeSettings,
|
||||
clock: RuntimeClockState
|
||||
): RuntimeResolvedTimeState {
|
||||
const timeOfDayHours = normalizeTimeOfDayHours(clock.timeOfDayHours);
|
||||
const dayPhase = resolveRuntimeDayPhase(settings, timeOfDayHours);
|
||||
|
||||
return {
|
||||
timeOfDayHours,
|
||||
dayCount: clock.dayCount,
|
||||
dayLengthMinutes: clock.dayLengthMinutes,
|
||||
dayPhase,
|
||||
isNight: dayPhase === "night"
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimeDayNightPhaseWeights(
|
||||
settings: ProjectTimeSettings,
|
||||
timeOfDayHours: number
|
||||
): RuntimeDayNightPhaseWeights {
|
||||
const dawnHalfDuration = Math.max(settings.dawnDurationHours, 0.001) / 2;
|
||||
const duskHalfDuration = Math.max(settings.duskDurationHours, 0.001) / 2;
|
||||
const dawnStart = settings.sunriseTimeOfDayHours - dawnHalfDuration;
|
||||
const boundaries = resolveRuntimeDayPhaseWindowBoundaries(settings);
|
||||
const dawnStart = boundaries.dawnStart;
|
||||
const currentTime = wrapTimeForward(timeOfDayHours, dawnStart);
|
||||
const sunrise = wrapTimeForward(settings.sunriseTimeOfDayHours, dawnStart);
|
||||
const dawnEnd = sunrise + dawnHalfDuration;
|
||||
const dawnEnd = wrapTimeForward(boundaries.dawnEnd, dawnStart);
|
||||
const sunset = wrapTimeForward(settings.sunsetTimeOfDayHours, dawnStart);
|
||||
const duskStart = sunset - duskHalfDuration;
|
||||
const duskEnd = sunset + duskHalfDuration;
|
||||
const duskStart = wrapTimeForward(boundaries.duskStart, dawnStart);
|
||||
const duskEnd = wrapTimeForward(boundaries.duskEnd, dawnStart);
|
||||
|
||||
if (currentTime < sunrise) {
|
||||
const amount = smoothstep(dawnStart, sunrise, currentTime);
|
||||
@@ -649,9 +821,11 @@ export function formatRuntimeClockTime(state: RuntimeClockState): string {
|
||||
export function resolveRuntimeDayNightWorldState(
|
||||
world: WorldSettings,
|
||||
settings: ProjectTimeSettings,
|
||||
clock: RuntimeClockState | null
|
||||
clock: RuntimeClockState | null,
|
||||
resolvedTime: RuntimeResolvedTimeState | null =
|
||||
clock === null ? null : resolveRuntimeTimeState(settings, clock)
|
||||
): RuntimeDayNightWorldState {
|
||||
if (clock === null || !world.projectTimeLightingEnabled) {
|
||||
if (clock === null || resolvedTime === null || !world.projectTimeLightingEnabled) {
|
||||
return {
|
||||
ambientLight: {
|
||||
colorHex: world.ambientLight.colorHex,
|
||||
@@ -671,7 +845,7 @@ export function resolveRuntimeDayNightWorldState(
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedTime = normalizeTimeOfDayHours(clock.timeOfDayHours);
|
||||
const normalizedTime = resolvedTime.timeOfDayHours;
|
||||
const phaseWeights = resolveRuntimeDayNightPhaseWeights(
|
||||
settings,
|
||||
normalizedTime
|
||||
|
||||
@@ -5,7 +5,12 @@ import { createDefaultWorldSettings } from "../../src/document/world-settings";
|
||||
import {
|
||||
advanceRuntimeClockState,
|
||||
createRuntimeClockState,
|
||||
hasTimeWindowJustEnded,
|
||||
hasTimeWindowJustStarted,
|
||||
isWithinTimeWindow,
|
||||
resolveRuntimeDayPhase,
|
||||
resolveRuntimeDayNightWorldState
|
||||
,resolveRuntimeTimeState
|
||||
} from "../../src/runtime-three/runtime-project-time";
|
||||
|
||||
describe("runtime project time", () => {
|
||||
@@ -23,6 +28,68 @@ describe("runtime project time", () => {
|
||||
expect(advancedClock.dayLengthMinutes).toBe(24);
|
||||
});
|
||||
|
||||
it("resolves reusable runtime day semantics from the authored sun windows", () => {
|
||||
const settings = createDefaultProjectTimeSettings();
|
||||
settings.startDayNumber = 4;
|
||||
settings.dayLengthMinutes = 30;
|
||||
settings.sunriseTimeOfDayHours = 7;
|
||||
settings.sunsetTimeOfDayHours = 20;
|
||||
settings.dawnDurationHours = 2;
|
||||
settings.duskDurationHours = 2;
|
||||
|
||||
const resolvedNight = resolveRuntimeTimeState(settings, {
|
||||
timeOfDayHours: 2,
|
||||
dayCount: 4,
|
||||
dayLengthMinutes: 30
|
||||
});
|
||||
const resolvedDawn = resolveRuntimeTimeState(settings, {
|
||||
timeOfDayHours: 6.5,
|
||||
dayCount: 4,
|
||||
dayLengthMinutes: 30
|
||||
});
|
||||
const resolvedDay = resolveRuntimeTimeState(settings, {
|
||||
timeOfDayHours: 12,
|
||||
dayCount: 4,
|
||||
dayLengthMinutes: 30
|
||||
});
|
||||
const resolvedDusk = resolveRuntimeTimeState(settings, {
|
||||
timeOfDayHours: 20.5,
|
||||
dayCount: 4,
|
||||
dayLengthMinutes: 30
|
||||
});
|
||||
|
||||
expect(resolveRuntimeDayPhase(settings, 2)).toBe("night");
|
||||
expect(resolveRuntimeDayPhase(settings, 6.5)).toBe("dawn");
|
||||
expect(resolveRuntimeDayPhase(settings, 12)).toBe("day");
|
||||
expect(resolveRuntimeDayPhase(settings, 20.5)).toBe("dusk");
|
||||
|
||||
expect(resolvedNight).toMatchObject({
|
||||
timeOfDayHours: 2,
|
||||
dayCount: 4,
|
||||
dayLengthMinutes: 30,
|
||||
dayPhase: "night",
|
||||
isNight: true
|
||||
});
|
||||
expect(resolvedDawn.dayPhase).toBe("dawn");
|
||||
expect(resolvedDawn.isNight).toBe(false);
|
||||
expect(resolvedDay.dayPhase).toBe("day");
|
||||
expect(resolvedDay.isNight).toBe(false);
|
||||
expect(resolvedDusk.dayPhase).toBe("dusk");
|
||||
});
|
||||
|
||||
it("handles forward time windows across midnight", () => {
|
||||
expect(isWithinTimeWindow(22, 2, 23.5)).toBe(true);
|
||||
expect(isWithinTimeWindow(22, 2, 1.5)).toBe(true);
|
||||
expect(isWithinTimeWindow(22, 2, 12)).toBe(false);
|
||||
|
||||
expect(hasTimeWindowJustStarted(21.5, 23.5, 22, 2)).toBe(true);
|
||||
expect(hasTimeWindowJustStarted(23.5, 1, 22, 2)).toBe(false);
|
||||
expect(hasTimeWindowJustEnded(23.5, 2.5, 22, 2)).toBe(true);
|
||||
|
||||
expect(hasTimeWindowJustStarted(21, 3, 22, 2)).toBe(true);
|
||||
expect(hasTimeWindowJustEnded(21, 3, 22, 2)).toBe(true);
|
||||
});
|
||||
|
||||
it("derives authored dawn, day, and night lighting from the global time profile", () => {
|
||||
const world = createDefaultWorldSettings();
|
||||
const time = createDefaultProjectTimeSettings();
|
||||
|
||||
Reference in New Issue
Block a user