Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/packages/editor-oss/src/EngineRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import type {RamPanelManager} from "./utils/RamPanelManager";
import {SceneLoadProfiler} from "./utils/SceneLoadProfiler";
import {findObjectsInRectangle} from "./utils/SelectionUtils";
import Storage from "./utils/Storage";
import {getViewportSafeArea, type ViewportSafeArea} from "./utils/viewportSafeArea";

// TODO: Move RectAreaLightTexturesLib initialization to appropriate place
RectAreaLightNode.setLTC(RectAreaLightTexturesLib.init());
Expand Down Expand Up @@ -110,6 +111,7 @@ export const BILLBOARD_BEHAVIOR_ID = "billboard";
export const CHARACTER_BEHAVIOR_ID = "character";
export const ENEMY_BEHAVIOR_ID = "enemy";
export const NPC_BEHAVIOR_ID = "npc";
export type {ViewportSafeArea} from "./utils/viewportSafeArea";

// Application have a lot of responsibilities, which can lead to high complexity and low maintainability.
// Consider splitting responsibilities to different classes or modules (e.g., SceneManager, ModeManager, etc.)
Expand All @@ -118,6 +120,8 @@ export class EngineRuntime extends AppRuntime implements RuntimeContext {
return window.location.pathname.indexOf("/sandbox/") !== -1;
}

private viewportSafeAreaElements = new Map<string, HTMLElement>();

// Make sure that we have clear interfaces instead of using field directly to assign values
// This will help reduce complexity and improve maintainability

Expand Down Expand Up @@ -216,6 +220,19 @@ export class EngineRuntime extends AppRuntime implements RuntimeContext {
get playerEvent(): PlayerEvent | null {
return this.playerSession?.playerEvent ?? null;
}

getViewportSafeArea(): ViewportSafeArea {
return getViewportSafeArea(this.viewport, Array.from(this.viewportSafeAreaElements.values()));
}

registerViewportSafeAreaElement(id: string, element: HTMLElement | null): void {
if (!id) return;
if (element) {
this.viewportSafeAreaElements.set(id, element);
return;
}
this.viewportSafeAreaElements.delete(id);
}
get aiWorldControl(): AiWorldControl | null {
return this.playerSession?.aiWorldControl ?? null;
}
Expand Down
5 changes: 5 additions & 0 deletions client/packages/editor-oss/src/behaviors/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import AIConversationManager from "../packs/aiNpc/AiConversationManager";
import {IMultiplayerState} from "../state/IMultiplayerState";
import UIKitPointerEvents from "../uikit/UIKitPointerEvents";
import {isLegacyBehaviorId} from "../util";
import type {ViewportSafeArea} from "../../utils/viewportSafeArea";

export interface IControl {
attachPlayerObject(player: Object3D, characterOptions: CharacterOptionsInterface): Promise<void>;
Expand Down Expand Up @@ -205,6 +206,10 @@ class GameManager {
}
}

public getViewportSafeArea(): ViewportSafeArea {
return this.engine.getViewportSafeArea();
}

/**
* Handle unified game services authentication success
* @param user - Authenticated user from unified game services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import { StemStore } from './store/StemStore';
import { StemTeam } from './team/StemTeam';
import { StemTween } from './tween/StemTween';
import type { Lambda } from '../../lambdas/Lambda';
import type {ViewportSafeArea} from '../../utils/viewportSafeArea';

export interface StemViewport {
/**
* Safe viewport rect in window coordinates.
* Useful for DOM overlays or UI projection that must avoid host chrome.
*/
getSafeArea(): ViewportSafeArea;
}

/**
* Lambda ECS system access for behaviors.
Expand Down Expand Up @@ -133,6 +142,8 @@ export interface StemEngineInterface {
camera: StemCamera;
/** 3D object creation from Three.js objects. */
object: StemObject;
/** Measured visible runtime viewport for overlay-safe DOM and screen-space UI. */
viewport: StemViewport;
/** Scene graph manipulation (adding objects). */
scene: StemScene;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import EngineRuntime from "@stem/editor-oss/EngineRuntime";
import { createForeignLambdaView } from "../../lambdas/Lambda";
import GameManager from "../game/GameManager";

const createViewportInterface = (game: GameManager) => ({
getSafeArea: () => game.getViewportSafeArea(),
});

const createLambdasInterface = (game: GameManager): StemLambdas => {
return {
getInstance: (instanceId: string) => {
Expand Down Expand Up @@ -66,6 +70,7 @@ export const createStemEngineInterface = (game: GameManager, globalStore: Global
team: createTeamInterface(),
pool: createPoolInterface(),
object: createObjectInterface(game),
viewport: createViewportInterface(game),
scene: createSceneInterface(game),
store: createStoreInterface(globalStore),
lambdas: createLambdasInterface(game),
Expand All @@ -92,6 +97,7 @@ export const createEditorErthInterface = (engine: EngineRuntime): StemEngineInte
ai: { generate: () => notAvailable('ai') } as any,
camera: { setTarget: () => notAvailable('camera'), getPosition: () => notAvailable('camera') } as any,
object: { create: () => notAvailable('object'), destroy: () => notAvailable('object') } as any,
viewport: { getSafeArea: () => engine.getViewportSafeArea() },
scene: { getObjects: () => notAvailable('scene') } as any,
lambdas: { getInstance: () => notAvailable('lambdas') } as any,
behaviors: { find: () => notAvailable('behaviors') } as any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ interface StemStore {
readonly size: number;
}

interface StemViewportSafeArea {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
insetLeft: number;
insetTop: number;
insetRight: number;
insetBottom: number;
}

interface StemViewport {
/** Safe runtime viewport bounds in window coordinates. */
getSafeArea(): StemViewportSafeArea;
}

/**
* Erth interface providing access to game subsystems.
* Access via this.erth in behavior scripts.
Expand All @@ -86,6 +104,9 @@ interface StemEngineInterface {
*/
store: StemStore;

/** Measured visible runtime viewport for overlay-safe DOM and screen-space UI. */
viewport: StemViewport;

/** Asset subsystem for loading images, audio, video, and models from the asset library */
asset: {
image: {
Expand Down Expand Up @@ -361,6 +382,9 @@ interface GameManager {
/** Check if game is started */
isGameStarted(): boolean;

/** Safe runtime viewport bounds in window coordinates. */
getViewportSafeArea(): StemViewportSafeArea;

/** Add object to scene */
addObject(object: THREE.Object3D, parent?: THREE.Object3D): Promise<void>;
/** Remove object from scene */
Expand Down
106 changes: 106 additions & 0 deletions client/packages/editor-oss/src/utils/viewportSafeArea.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {describe, expect, it} from "vitest";

import {measureViewportSafeArea} from "./viewportSafeArea";

describe("measureViewportSafeArea", () => {
it("falls back to the full window when no viewport is available", () => {
expect(measureViewportSafeArea({windowWidth: 1280, windowHeight: 720})).toEqual({
left: 0,
top: 0,
right: 1280,
bottom: 720,
width: 1280,
height: 720,
insetLeft: 0,
insetTop: 0,
insetRight: 0,
insetBottom: 0,
});
});

it("derives safe insets from the viewport rect", () => {
expect(measureViewportSafeArea({
windowWidth: 1280,
windowHeight: 720,
rect: {
left: 24,
top: 48,
width: 1200,
height: 640,
right: 1224,
bottom: 688,
},
})).toEqual({
left: 24,
top: 48,
right: 1224,
bottom: 688,
width: 1200,
height: 640,
insetLeft: 24,
insetTop: 48,
insetRight: 56,
insetBottom: 32,
});
});

it("clamps an oversized rect back into the window bounds", () => {
expect(measureViewportSafeArea({
windowWidth: 1000,
windowHeight: 600,
rect: {
left: -20,
top: 40,
width: 1100,
height: 700,
right: 1080,
bottom: 740,
},
})).toEqual({
left: 0,
top: 40,
right: 1000,
bottom: 600,
width: 1000,
height: 560,
insetLeft: 0,
insetTop: 40,
insetRight: 0,
insetBottom: 0,
});
});

it("shrinks the safe area for edge-anchored host overlays inside the viewport", () => {
expect(measureViewportSafeArea({
windowWidth: 1280,
windowHeight: 720,
rect: {
left: 0,
top: 48,
width: 1280,
height: 672,
right: 1280,
bottom: 720,
},
occluders: [{
left: 1160,
top: 676,
width: 108,
height: 32,
right: 1268,
bottom: 708,
}],
})).toEqual({
left: 0,
top: 48,
right: 1160,
bottom: 676,
width: 1160,
height: 628,
insetLeft: 0,
insetTop: 48,
insetRight: 120,
insetBottom: 44,
});
});
Comment thread
querielo marked this conversation as resolved.
});
Loading
Loading