928 lines
24 KiB
TypeScript
928 lines
24 KiB
TypeScript
export const CONTROL_ENTITY_TARGET_KINDS = [
|
|
"pointLight",
|
|
"spotLight",
|
|
"soundEmitter"
|
|
] as const;
|
|
export const CONTROL_INTERACTION_TARGET_KINDS = [
|
|
"interactable",
|
|
"sceneExit"
|
|
] as const;
|
|
export const CONTROL_CAPABILITY_KINDS = [
|
|
"animationPlayback",
|
|
"soundPlayback",
|
|
"interactionAvailability",
|
|
"lightEnabled",
|
|
"lightIntensity"
|
|
] as const;
|
|
|
|
export type ControlEntityTargetKind =
|
|
(typeof CONTROL_ENTITY_TARGET_KINDS)[number];
|
|
export type ControlInteractionTargetKind =
|
|
(typeof CONTROL_INTERACTION_TARGET_KINDS)[number];
|
|
export type ControlCapabilityKind = (typeof CONTROL_CAPABILITY_KINDS)[number];
|
|
|
|
export interface ActorControlTargetRef {
|
|
kind: "actor";
|
|
actorId: string;
|
|
}
|
|
|
|
export interface EntityControlTargetRef<
|
|
TKind extends ControlEntityTargetKind = ControlEntityTargetKind
|
|
> {
|
|
kind: "entity";
|
|
entityId: string;
|
|
entityKind: TKind;
|
|
}
|
|
|
|
export interface InteractionControlTargetRef<
|
|
TKind extends ControlInteractionTargetKind = ControlInteractionTargetKind
|
|
> {
|
|
kind: "interaction";
|
|
entityId: string;
|
|
interactionKind: TKind;
|
|
}
|
|
|
|
export interface SceneControlTargetRef {
|
|
kind: "scene";
|
|
scope: "activeScene";
|
|
}
|
|
|
|
export interface GlobalControlTargetRef {
|
|
kind: "global";
|
|
scope: "project";
|
|
}
|
|
|
|
export interface ModelInstanceControlTargetRef {
|
|
kind: "modelInstance";
|
|
modelInstanceId: string;
|
|
}
|
|
|
|
export type LightControlTargetRef = EntityControlTargetRef<
|
|
"pointLight" | "spotLight"
|
|
>;
|
|
export type SoundEmitterControlTargetRef =
|
|
EntityControlTargetRef<"soundEmitter">;
|
|
export type ControlTargetRef =
|
|
| ActorControlTargetRef
|
|
| EntityControlTargetRef
|
|
| InteractionControlTargetRef
|
|
| SceneControlTargetRef
|
|
| GlobalControlTargetRef
|
|
| ModelInstanceControlTargetRef;
|
|
|
|
export interface ControlTargetDescriptor {
|
|
target: ControlTargetRef;
|
|
capabilities: ControlCapabilityKind[];
|
|
}
|
|
|
|
export interface PlayModelAnimationControlEffect {
|
|
type: "playModelAnimation";
|
|
target: ModelInstanceControlTargetRef;
|
|
clipName: string;
|
|
loop?: boolean;
|
|
}
|
|
|
|
export interface StopModelAnimationControlEffect {
|
|
type: "stopModelAnimation";
|
|
target: ModelInstanceControlTargetRef;
|
|
}
|
|
|
|
export interface PlaySoundControlEffect {
|
|
type: "playSound";
|
|
target: SoundEmitterControlTargetRef;
|
|
}
|
|
|
|
export interface StopSoundControlEffect {
|
|
type: "stopSound";
|
|
target: SoundEmitterControlTargetRef;
|
|
}
|
|
|
|
export interface SetInteractionEnabledControlEffect {
|
|
type: "setInteractionEnabled";
|
|
target: InteractionControlTargetRef;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface SetLightEnabledControlEffect {
|
|
type: "setLightEnabled";
|
|
target: LightControlTargetRef;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface SetLightIntensityControlEffect {
|
|
type: "setLightIntensity";
|
|
target: LightControlTargetRef;
|
|
intensity: number;
|
|
}
|
|
|
|
export type ControlEffect =
|
|
| PlayModelAnimationControlEffect
|
|
| StopModelAnimationControlEffect
|
|
| PlaySoundControlEffect
|
|
| StopSoundControlEffect
|
|
| SetInteractionEnabledControlEffect
|
|
| SetLightEnabledControlEffect
|
|
| SetLightIntensityControlEffect;
|
|
|
|
export interface LightIntensityControlChannelDescriptor {
|
|
channel: "light.intensity";
|
|
target: LightControlTargetRef;
|
|
minValue: number;
|
|
defaultValue: number;
|
|
}
|
|
|
|
export type ControlChannelDescriptor = LightIntensityControlChannelDescriptor;
|
|
|
|
export interface DefaultResolvedControlSource {
|
|
kind: "default";
|
|
}
|
|
|
|
export interface InteractionLinkResolvedControlSource {
|
|
kind: "interactionLink";
|
|
linkId: string;
|
|
}
|
|
|
|
export interface SchedulerResolvedControlSource {
|
|
kind: "scheduler";
|
|
scheduleId: string;
|
|
}
|
|
|
|
export type RuntimeResolvedControlSource =
|
|
| DefaultResolvedControlSource
|
|
| InteractionLinkResolvedControlSource
|
|
| SchedulerResolvedControlSource;
|
|
|
|
export interface RuntimeResolvedLightEnabledState {
|
|
type: "lightEnabled";
|
|
target: LightControlTargetRef;
|
|
value: boolean;
|
|
source: RuntimeResolvedControlSource;
|
|
}
|
|
|
|
export interface RuntimeResolvedInteractionEnabledState {
|
|
type: "interactionEnabled";
|
|
target: InteractionControlTargetRef;
|
|
value: boolean;
|
|
source: RuntimeResolvedControlSource;
|
|
}
|
|
|
|
export type RuntimeResolvedDiscreteControlState =
|
|
| RuntimeResolvedLightEnabledState
|
|
| RuntimeResolvedInteractionEnabledState;
|
|
|
|
export interface RuntimeResolvedLightIntensityChannelValue {
|
|
type: "lightIntensity";
|
|
descriptor: LightIntensityControlChannelDescriptor;
|
|
value: number;
|
|
source: RuntimeResolvedControlSource;
|
|
}
|
|
|
|
export type RuntimeResolvedControlChannelValue =
|
|
RuntimeResolvedLightIntensityChannelValue;
|
|
|
|
export interface RuntimeResolvedControlState {
|
|
discrete: RuntimeResolvedDiscreteControlState[];
|
|
channels: RuntimeResolvedControlChannelValue[];
|
|
}
|
|
|
|
export interface RuntimeControlSurfaceDefinition {
|
|
targets: ControlTargetDescriptor[];
|
|
channels: ControlChannelDescriptor[];
|
|
resolved: RuntimeResolvedControlState;
|
|
}
|
|
|
|
function assertNonEmptyString(value: string, label: string) {
|
|
if (value.trim().length === 0) {
|
|
throw new Error(`${label} must be non-empty.`);
|
|
}
|
|
}
|
|
|
|
function assertNonNegativeFiniteNumber(value: number, label: string) {
|
|
if (!Number.isFinite(value) || value < 0) {
|
|
throw new Error(`${label} must be a finite number greater than or equal to zero.`);
|
|
}
|
|
}
|
|
|
|
function assertBoolean(value: boolean, label: string) {
|
|
if (typeof value !== "boolean") {
|
|
throw new Error(`${label} must be a boolean.`);
|
|
}
|
|
}
|
|
|
|
export function isControlEntityTargetKind(
|
|
value: unknown
|
|
): value is ControlEntityTargetKind {
|
|
return CONTROL_ENTITY_TARGET_KINDS.includes(
|
|
value as ControlEntityTargetKind
|
|
);
|
|
}
|
|
|
|
export function isControlInteractionTargetKind(
|
|
value: unknown
|
|
): value is ControlInteractionTargetKind {
|
|
return CONTROL_INTERACTION_TARGET_KINDS.includes(
|
|
value as ControlInteractionTargetKind
|
|
);
|
|
}
|
|
|
|
export function createActorControlTargetRef(
|
|
actorId: string
|
|
): ActorControlTargetRef {
|
|
assertNonEmptyString(actorId, "Control actor id");
|
|
|
|
return {
|
|
kind: "actor",
|
|
actorId
|
|
};
|
|
}
|
|
|
|
export function createEntityControlTargetRef<
|
|
TKind extends ControlEntityTargetKind
|
|
>(entityKind: TKind, entityId: string): EntityControlTargetRef<TKind> {
|
|
assertNonEmptyString(entityId, "Control entity id");
|
|
|
|
return {
|
|
kind: "entity",
|
|
entityId,
|
|
entityKind
|
|
};
|
|
}
|
|
|
|
export function createInteractionControlTargetRef<
|
|
TKind extends ControlInteractionTargetKind
|
|
>(
|
|
interactionKind: TKind,
|
|
entityId: string
|
|
): InteractionControlTargetRef<TKind> {
|
|
assertNonEmptyString(entityId, "Control interaction entity id");
|
|
|
|
return {
|
|
kind: "interaction",
|
|
entityId,
|
|
interactionKind
|
|
};
|
|
}
|
|
|
|
export function createActiveSceneControlTargetRef(): SceneControlTargetRef {
|
|
return {
|
|
kind: "scene",
|
|
scope: "activeScene"
|
|
};
|
|
}
|
|
|
|
export function createProjectGlobalControlTargetRef(): GlobalControlTargetRef {
|
|
return {
|
|
kind: "global",
|
|
scope: "project"
|
|
};
|
|
}
|
|
|
|
export function createModelInstanceControlTargetRef(
|
|
modelInstanceId: string
|
|
): ModelInstanceControlTargetRef {
|
|
assertNonEmptyString(modelInstanceId, "Control model instance id");
|
|
|
|
return {
|
|
kind: "modelInstance",
|
|
modelInstanceId
|
|
};
|
|
}
|
|
|
|
export function createLightControlTargetRef(
|
|
lightKind: "pointLight" | "spotLight",
|
|
entityId: string
|
|
): LightControlTargetRef {
|
|
return createEntityControlTargetRef(lightKind, entityId);
|
|
}
|
|
|
|
export function createSoundEmitterControlTargetRef(
|
|
entityId: string
|
|
): SoundEmitterControlTargetRef {
|
|
return createEntityControlTargetRef("soundEmitter", entityId);
|
|
}
|
|
|
|
export function createControlTargetDescriptor(
|
|
target: ControlTargetRef,
|
|
capabilities: ControlCapabilityKind[]
|
|
): ControlTargetDescriptor {
|
|
return {
|
|
target: cloneControlTargetRef(target),
|
|
capabilities: [...capabilities]
|
|
};
|
|
}
|
|
|
|
export function createPlayModelAnimationControlEffect(options: {
|
|
target: ModelInstanceControlTargetRef;
|
|
clipName: string;
|
|
loop?: boolean;
|
|
}): PlayModelAnimationControlEffect {
|
|
assertNonEmptyString(options.clipName, "Control animation clip name");
|
|
|
|
return {
|
|
type: "playModelAnimation",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as ModelInstanceControlTargetRef,
|
|
clipName: options.clipName,
|
|
loop: options.loop
|
|
};
|
|
}
|
|
|
|
export function createStopModelAnimationControlEffect(options: {
|
|
target: ModelInstanceControlTargetRef;
|
|
}): StopModelAnimationControlEffect {
|
|
return {
|
|
type: "stopModelAnimation",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as ModelInstanceControlTargetRef
|
|
};
|
|
}
|
|
|
|
export function createPlaySoundControlEffect(options: {
|
|
target: SoundEmitterControlTargetRef;
|
|
}): PlaySoundControlEffect {
|
|
return {
|
|
type: "playSound",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as SoundEmitterControlTargetRef
|
|
};
|
|
}
|
|
|
|
export function createStopSoundControlEffect(options: {
|
|
target: SoundEmitterControlTargetRef;
|
|
}): StopSoundControlEffect {
|
|
return {
|
|
type: "stopSound",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as SoundEmitterControlTargetRef
|
|
};
|
|
}
|
|
|
|
export function createSetInteractionEnabledControlEffect(options: {
|
|
target: InteractionControlTargetRef;
|
|
enabled: boolean;
|
|
}): SetInteractionEnabledControlEffect {
|
|
assertBoolean(options.enabled, "Control interaction enabled");
|
|
|
|
return {
|
|
type: "setInteractionEnabled",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as InteractionControlTargetRef,
|
|
enabled: options.enabled
|
|
};
|
|
}
|
|
|
|
export function createSetLightEnabledControlEffect(options: {
|
|
target: LightControlTargetRef;
|
|
enabled: boolean;
|
|
}): SetLightEnabledControlEffect {
|
|
assertBoolean(options.enabled, "Control light enabled");
|
|
|
|
return {
|
|
type: "setLightEnabled",
|
|
target: cloneControlTargetRef(options.target) as LightControlTargetRef,
|
|
enabled: options.enabled
|
|
};
|
|
}
|
|
|
|
export function createSetLightIntensityControlEffect(options: {
|
|
target: LightControlTargetRef;
|
|
intensity: number;
|
|
}): SetLightIntensityControlEffect {
|
|
assertNonNegativeFiniteNumber(options.intensity, "Control light intensity");
|
|
|
|
return {
|
|
type: "setLightIntensity",
|
|
target: cloneControlTargetRef(options.target) as LightControlTargetRef,
|
|
intensity: options.intensity
|
|
};
|
|
}
|
|
|
|
export function createLightIntensityControlChannelDescriptor(options: {
|
|
target: LightControlTargetRef;
|
|
defaultValue: number;
|
|
minValue?: number;
|
|
}): LightIntensityControlChannelDescriptor {
|
|
const minValue = options.minValue ?? 0;
|
|
assertNonNegativeFiniteNumber(
|
|
options.defaultValue,
|
|
"Control light intensity default"
|
|
);
|
|
assertNonNegativeFiniteNumber(
|
|
minValue,
|
|
"Control light intensity minimum"
|
|
);
|
|
|
|
return {
|
|
channel: "light.intensity",
|
|
target: cloneControlTargetRef(options.target) as LightControlTargetRef,
|
|
minValue,
|
|
defaultValue: options.defaultValue
|
|
};
|
|
}
|
|
|
|
export function createDefaultResolvedControlSource(): DefaultResolvedControlSource {
|
|
return {
|
|
kind: "default"
|
|
};
|
|
}
|
|
|
|
export function createInteractionLinkResolvedControlSource(
|
|
linkId: string
|
|
): InteractionLinkResolvedControlSource {
|
|
assertNonEmptyString(linkId, "Resolved control interaction link id");
|
|
|
|
return {
|
|
kind: "interactionLink",
|
|
linkId
|
|
};
|
|
}
|
|
|
|
export function createResolvedLightEnabledState(options: {
|
|
target: LightControlTargetRef;
|
|
value: boolean;
|
|
source: RuntimeResolvedControlSource;
|
|
}): RuntimeResolvedLightEnabledState {
|
|
assertBoolean(options.value, "Resolved control light enabled");
|
|
|
|
return {
|
|
type: "lightEnabled",
|
|
target: cloneControlTargetRef(options.target) as LightControlTargetRef,
|
|
value: options.value,
|
|
source: cloneResolvedControlSource(options.source)
|
|
};
|
|
}
|
|
|
|
export function createResolvedInteractionEnabledState(options: {
|
|
target: InteractionControlTargetRef;
|
|
value: boolean;
|
|
source: RuntimeResolvedControlSource;
|
|
}): RuntimeResolvedInteractionEnabledState {
|
|
assertBoolean(options.value, "Resolved control interaction enabled");
|
|
|
|
return {
|
|
type: "interactionEnabled",
|
|
target: cloneControlTargetRef(
|
|
options.target
|
|
) as InteractionControlTargetRef,
|
|
value: options.value,
|
|
source: cloneResolvedControlSource(options.source)
|
|
};
|
|
}
|
|
|
|
export function createResolvedLightIntensityChannelValue(options: {
|
|
descriptor: LightIntensityControlChannelDescriptor;
|
|
value: number;
|
|
source: RuntimeResolvedControlSource;
|
|
}): RuntimeResolvedLightIntensityChannelValue {
|
|
assertNonNegativeFiniteNumber(
|
|
options.value,
|
|
"Resolved control light intensity"
|
|
);
|
|
|
|
return {
|
|
type: "lightIntensity",
|
|
descriptor: cloneControlChannelDescriptor(
|
|
options.descriptor
|
|
) as LightIntensityControlChannelDescriptor,
|
|
value: options.value,
|
|
source: cloneResolvedControlSource(options.source)
|
|
};
|
|
}
|
|
|
|
export function createEmptyRuntimeResolvedControlState(): RuntimeResolvedControlState {
|
|
return {
|
|
discrete: [],
|
|
channels: []
|
|
};
|
|
}
|
|
|
|
export function createEmptyRuntimeControlSurfaceDefinition(): RuntimeControlSurfaceDefinition {
|
|
return {
|
|
targets: [],
|
|
channels: [],
|
|
resolved: createEmptyRuntimeResolvedControlState()
|
|
};
|
|
}
|
|
|
|
export function createRuntimeControlSurfaceDefinition(options: {
|
|
targets: ControlTargetDescriptor[];
|
|
channels?: ControlChannelDescriptor[];
|
|
resolved?: RuntimeResolvedControlState;
|
|
}): RuntimeControlSurfaceDefinition {
|
|
return {
|
|
targets: options.targets.map(cloneControlTargetDescriptor),
|
|
channels: (options.channels ?? []).map(cloneControlChannelDescriptor),
|
|
resolved: cloneRuntimeResolvedControlState(
|
|
options.resolved ?? createEmptyRuntimeResolvedControlState()
|
|
)
|
|
};
|
|
}
|
|
|
|
export function getControlTargetRefKey(target: ControlTargetRef): string {
|
|
switch (target.kind) {
|
|
case "actor":
|
|
return `actor:${target.actorId}`;
|
|
case "entity":
|
|
return `entity:${target.entityKind}:${target.entityId}`;
|
|
case "interaction":
|
|
return `interaction:${target.interactionKind}:${target.entityId}`;
|
|
case "scene":
|
|
return `scene:${target.scope}`;
|
|
case "global":
|
|
return `global:${target.scope}`;
|
|
case "modelInstance":
|
|
return `modelInstance:${target.modelInstanceId}`;
|
|
}
|
|
}
|
|
|
|
export function getControlChannelDescriptorKey(
|
|
descriptor: ControlChannelDescriptor
|
|
): string {
|
|
switch (descriptor.channel) {
|
|
case "light.intensity":
|
|
return `channel:${descriptor.channel}:${getControlTargetRefKey(
|
|
descriptor.target
|
|
)}`;
|
|
}
|
|
}
|
|
|
|
function getResolvedDiscreteControlStateKey(
|
|
state: RuntimeResolvedDiscreteControlState
|
|
): string {
|
|
switch (state.type) {
|
|
case "lightEnabled":
|
|
return `state:${state.type}:${getControlTargetRefKey(state.target)}`;
|
|
case "interactionEnabled":
|
|
return `state:${state.type}:${getControlTargetRefKey(state.target)}`;
|
|
}
|
|
}
|
|
|
|
export function cloneControlTargetRef<TTarget extends ControlTargetRef>(
|
|
target: TTarget
|
|
): TTarget {
|
|
switch (target.kind) {
|
|
case "actor":
|
|
return {
|
|
kind: "actor",
|
|
actorId: target.actorId
|
|
} as TTarget;
|
|
case "entity":
|
|
return {
|
|
kind: "entity",
|
|
entityId: target.entityId,
|
|
entityKind: target.entityKind
|
|
} as TTarget;
|
|
case "interaction":
|
|
return {
|
|
kind: "interaction",
|
|
entityId: target.entityId,
|
|
interactionKind: target.interactionKind
|
|
} as TTarget;
|
|
case "scene":
|
|
return {
|
|
kind: "scene",
|
|
scope: target.scope
|
|
} as TTarget;
|
|
case "global":
|
|
return {
|
|
kind: "global",
|
|
scope: target.scope
|
|
} as TTarget;
|
|
case "modelInstance":
|
|
return {
|
|
kind: "modelInstance",
|
|
modelInstanceId: target.modelInstanceId
|
|
} as TTarget;
|
|
}
|
|
}
|
|
|
|
export function cloneControlTargetDescriptor(
|
|
descriptor: ControlTargetDescriptor
|
|
): ControlTargetDescriptor {
|
|
return {
|
|
target: cloneControlTargetRef(descriptor.target),
|
|
capabilities: [...descriptor.capabilities]
|
|
};
|
|
}
|
|
|
|
export function cloneResolvedControlSource(
|
|
source: RuntimeResolvedControlSource
|
|
): RuntimeResolvedControlSource {
|
|
switch (source.kind) {
|
|
case "default":
|
|
return {
|
|
kind: "default"
|
|
};
|
|
case "interactionLink":
|
|
return {
|
|
kind: "interactionLink",
|
|
linkId: source.linkId
|
|
};
|
|
case "scheduler":
|
|
return {
|
|
kind: "scheduler",
|
|
scheduleId: source.scheduleId
|
|
};
|
|
}
|
|
}
|
|
|
|
export function cloneControlEffect<TEffect extends ControlEffect>(
|
|
effect: TEffect
|
|
): TEffect {
|
|
switch (effect.type) {
|
|
case "playModelAnimation":
|
|
return {
|
|
type: "playModelAnimation",
|
|
target: cloneControlTargetRef(effect.target),
|
|
clipName: effect.clipName,
|
|
loop: effect.loop
|
|
} as TEffect;
|
|
case "stopModelAnimation":
|
|
return {
|
|
type: "stopModelAnimation",
|
|
target: cloneControlTargetRef(effect.target)
|
|
} as TEffect;
|
|
case "playSound":
|
|
return {
|
|
type: "playSound",
|
|
target: cloneControlTargetRef(effect.target)
|
|
} as TEffect;
|
|
case "stopSound":
|
|
return {
|
|
type: "stopSound",
|
|
target: cloneControlTargetRef(effect.target)
|
|
} as TEffect;
|
|
case "setInteractionEnabled":
|
|
return {
|
|
type: "setInteractionEnabled",
|
|
target: cloneControlTargetRef(effect.target),
|
|
enabled: effect.enabled
|
|
} as TEffect;
|
|
case "setLightEnabled":
|
|
return {
|
|
type: "setLightEnabled",
|
|
target: cloneControlTargetRef(effect.target),
|
|
enabled: effect.enabled
|
|
} as TEffect;
|
|
case "setLightIntensity":
|
|
return {
|
|
type: "setLightIntensity",
|
|
target: cloneControlTargetRef(effect.target),
|
|
intensity: effect.intensity
|
|
} as TEffect;
|
|
}
|
|
}
|
|
|
|
export function cloneControlChannelDescriptor<
|
|
TDescriptor extends ControlChannelDescriptor
|
|
>(descriptor: TDescriptor): TDescriptor {
|
|
switch (descriptor.channel) {
|
|
case "light.intensity":
|
|
return {
|
|
channel: "light.intensity",
|
|
target: cloneControlTargetRef(descriptor.target),
|
|
minValue: descriptor.minValue,
|
|
defaultValue: descriptor.defaultValue
|
|
} as TDescriptor;
|
|
}
|
|
}
|
|
|
|
export function cloneRuntimeResolvedDiscreteControlState<
|
|
TState extends RuntimeResolvedDiscreteControlState
|
|
>(state: TState): TState {
|
|
switch (state.type) {
|
|
case "lightEnabled":
|
|
return createResolvedLightEnabledState({
|
|
target: state.target,
|
|
value: state.value,
|
|
source: state.source
|
|
}) as TState;
|
|
case "interactionEnabled":
|
|
return createResolvedInteractionEnabledState({
|
|
target: state.target,
|
|
value: state.value,
|
|
source: state.source
|
|
}) as TState;
|
|
}
|
|
}
|
|
|
|
export function cloneRuntimeResolvedControlChannelValue<
|
|
TValue extends RuntimeResolvedControlChannelValue
|
|
>(value: TValue): TValue {
|
|
switch (value.type) {
|
|
case "lightIntensity":
|
|
return createResolvedLightIntensityChannelValue({
|
|
descriptor: value.descriptor,
|
|
value: value.value,
|
|
source: value.source
|
|
}) as TValue;
|
|
}
|
|
}
|
|
|
|
export function cloneRuntimeResolvedControlState(
|
|
state: RuntimeResolvedControlState
|
|
): RuntimeResolvedControlState {
|
|
return {
|
|
discrete: state.discrete.map(cloneRuntimeResolvedDiscreteControlState),
|
|
channels: state.channels.map(cloneRuntimeResolvedControlChannelValue)
|
|
};
|
|
}
|
|
|
|
export function cloneRuntimeControlSurfaceDefinition(
|
|
controlSurface: RuntimeControlSurfaceDefinition
|
|
): RuntimeControlSurfaceDefinition {
|
|
return {
|
|
targets: controlSurface.targets.map(cloneControlTargetDescriptor),
|
|
channels: controlSurface.channels.map(cloneControlChannelDescriptor),
|
|
resolved: cloneRuntimeResolvedControlState(controlSurface.resolved)
|
|
};
|
|
}
|
|
|
|
export function areControlEffectsEqual(
|
|
left: ControlEffect,
|
|
right: ControlEffect
|
|
): boolean {
|
|
if (left.type !== right.type) {
|
|
return false;
|
|
}
|
|
|
|
if (getControlTargetRefKey(left.target) !== getControlTargetRefKey(right.target)) {
|
|
return false;
|
|
}
|
|
|
|
switch (left.type) {
|
|
case "playModelAnimation":
|
|
return (
|
|
left.clipName === (right as PlayModelAnimationControlEffect).clipName &&
|
|
left.loop === (right as PlayModelAnimationControlEffect).loop
|
|
);
|
|
case "stopModelAnimation":
|
|
case "playSound":
|
|
case "stopSound":
|
|
return true;
|
|
case "setInteractionEnabled":
|
|
return (
|
|
left.enabled ===
|
|
(right as SetInteractionEnabledControlEffect).enabled
|
|
);
|
|
case "setLightEnabled":
|
|
return left.enabled === (right as SetLightEnabledControlEffect).enabled;
|
|
case "setLightIntensity":
|
|
return (
|
|
left.intensity === (right as SetLightIntensityControlEffect).intensity
|
|
);
|
|
}
|
|
}
|
|
|
|
export function getControlEffectLabel(effect: ControlEffect): string {
|
|
switch (effect.type) {
|
|
case "playModelAnimation":
|
|
return "Play Animation";
|
|
case "stopModelAnimation":
|
|
return "Stop Animation";
|
|
case "playSound":
|
|
return "Play Sound";
|
|
case "stopSound":
|
|
return "Stop Sound";
|
|
case "setInteractionEnabled":
|
|
return "Set Interaction Enabled";
|
|
case "setLightEnabled":
|
|
return "Set Light Enabled";
|
|
case "setLightIntensity":
|
|
return "Set Light Intensity";
|
|
}
|
|
}
|
|
|
|
export function formatControlTargetRef(target: ControlTargetRef): string {
|
|
switch (target.kind) {
|
|
case "actor":
|
|
return `Actor ${target.actorId}`;
|
|
case "entity":
|
|
return `${formatTargetKindLabel(target.entityKind)} ${target.entityId}`;
|
|
case "interaction":
|
|
return `${formatTargetKindLabel(target.interactionKind)} ${target.entityId}`;
|
|
case "scene":
|
|
return "Active Scene";
|
|
case "global":
|
|
return "Project";
|
|
case "modelInstance":
|
|
return `Model Instance ${target.modelInstanceId}`;
|
|
}
|
|
}
|
|
|
|
function formatTargetKindLabel(
|
|
kind: ControlEntityTargetKind | ControlInteractionTargetKind
|
|
): string {
|
|
switch (kind) {
|
|
case "pointLight":
|
|
return "Point Light";
|
|
case "spotLight":
|
|
return "Spot Light";
|
|
case "soundEmitter":
|
|
return "Sound Emitter";
|
|
case "interactable":
|
|
return "Interactable";
|
|
case "sceneExit":
|
|
return "Scene Exit";
|
|
}
|
|
}
|
|
|
|
export function formatControlEffectValue(effect: ControlEffect): string {
|
|
switch (effect.type) {
|
|
case "playModelAnimation":
|
|
return effect.loop === false
|
|
? `${effect.clipName} (Once)`
|
|
: `${effect.clipName} (Loop)`;
|
|
case "stopModelAnimation":
|
|
return "Stop";
|
|
case "playSound":
|
|
return "Play";
|
|
case "stopSound":
|
|
return "Stop";
|
|
case "setInteractionEnabled":
|
|
case "setLightEnabled":
|
|
return effect.enabled ? "Enabled" : "Disabled";
|
|
case "setLightIntensity":
|
|
return String(effect.intensity);
|
|
}
|
|
}
|
|
|
|
export function applyControlEffectToResolvedState(
|
|
resolved: RuntimeResolvedControlState,
|
|
effect: ControlEffect,
|
|
source: RuntimeResolvedControlSource
|
|
): RuntimeResolvedControlState {
|
|
const nextResolved = cloneRuntimeResolvedControlState(resolved);
|
|
|
|
switch (effect.type) {
|
|
case "setLightEnabled": {
|
|
const nextState = createResolvedLightEnabledState({
|
|
target: effect.target,
|
|
value: effect.enabled,
|
|
source
|
|
});
|
|
const stateKey = getResolvedDiscreteControlStateKey(nextState);
|
|
const existingIndex = nextResolved.discrete.findIndex(
|
|
(candidate) => getResolvedDiscreteControlStateKey(candidate) === stateKey
|
|
);
|
|
|
|
if (existingIndex >= 0) {
|
|
nextResolved.discrete[existingIndex] = nextState;
|
|
} else {
|
|
nextResolved.discrete.push(nextState);
|
|
}
|
|
return nextResolved;
|
|
}
|
|
case "setInteractionEnabled": {
|
|
const nextState = createResolvedInteractionEnabledState({
|
|
target: effect.target,
|
|
value: effect.enabled,
|
|
source
|
|
});
|
|
const stateKey = getResolvedDiscreteControlStateKey(nextState);
|
|
const existingIndex = nextResolved.discrete.findIndex(
|
|
(candidate) => getResolvedDiscreteControlStateKey(candidate) === stateKey
|
|
);
|
|
|
|
if (existingIndex >= 0) {
|
|
nextResolved.discrete[existingIndex] = nextState;
|
|
} else {
|
|
nextResolved.discrete.push(nextState);
|
|
}
|
|
return nextResolved;
|
|
}
|
|
case "setLightIntensity": {
|
|
const nextValue = createResolvedLightIntensityChannelValue({
|
|
descriptor: createLightIntensityControlChannelDescriptor({
|
|
target: effect.target,
|
|
defaultValue: effect.intensity
|
|
}),
|
|
value: effect.intensity,
|
|
source
|
|
});
|
|
const descriptorKey = getControlChannelDescriptorKey(nextValue.descriptor);
|
|
const existingIndex = nextResolved.channels.findIndex(
|
|
(candidate) =>
|
|
getControlChannelDescriptorKey(candidate.descriptor) === descriptorKey
|
|
);
|
|
|
|
if (existingIndex >= 0) {
|
|
nextResolved.channels[existingIndex] = nextValue;
|
|
} else {
|
|
nextResolved.channels.push(nextValue);
|
|
}
|
|
return nextResolved;
|
|
}
|
|
case "playModelAnimation":
|
|
case "stopModelAnimation":
|
|
case "playSound":
|
|
case "stopSound":
|
|
return nextResolved;
|
|
}
|
|
}
|