OctanePluginApi.ts 8.8 Ko
Télécharger le fichier
import { FurnitureStackHeightComposer, GetRoomEngine, TextureUtils } from '@octane/renderer';
import { CreateLinkEvent, GetRoomSession, SendMessageComposer, VisitDesktop } from '../../api';
/**
* Plugin descriptor registered by external plugin scripts.
*/
export interface IOctanePlugin {
/** Unique plugin name */
name: string;
/** Label shown on the button in room tools */
label: string;
/** CSS class for the icon (octane-icon class) */
icon?: string;
/** Called when the plugin button is clicked */
onOpen: () => void;
/** Called to close/destroy the plugin UI */
onClose?: () => void;
/** Called when the plugin is first loaded, receives the Octane API */
onInit?: (api: IOctanePluginApi) => void;
}
/**
* API exposed to external plugins via window.OctanePlugins
*/
export interface IOctanePluginApi {
/** Register a plugin */
register: (plugin: IOctanePlugin) => void;
/** Unregister a plugin by name */
unregister: (name: string) => void;
/** Get all registered plugins */
getPlugins: () => IOctanePlugin[];
/** Fire a Octane link event (e.g., 'navigator/toggle-room-info') */
createLinkEvent: (link: string) => void;
/** Get the room engine instance */
getRoomEngine: () => ReturnType<typeof GetRoomEngine>;
/** Get the current room session */
getRoomSession: () => ReturnType<typeof GetRoomSession>;
/** Send a message composer to the server */
sendMessage: typeof SendMessageComposer;
/** Send a chat message to the server (processed as command if starts with ':') */
sendChat: (text: string, styleId?: number) => void;
/** Send stack height update for a furniture item (objectId, heightInCentimeters) */
sendStackHeight: (objectId: number, height: number) => void;
/** Take a screenshot of the room and download it as PNG */
takeScreenshot: () => Promise<void>;
/** Leave the room and go to hotel view */
visitDesktop: () => void;
/** Create a draggable floating window and return its container element */
createWindow: (id: string, title: string, width: number) => HTMLDivElement | null;
/** Destroy a floating window by id */
destroyWindow: (id: string) => void;
}
// Internal plugin storage
const _plugins: IOctanePlugin[] = [];
const _listeners: Array<() => void> = [];
const _windowCleanup = new Map<string, () => void>();
// One listener that throws must not stop the others from hearing the change.
function notifyListeners() {
_listeners.forEach((fn) => {
try {
fn();
} catch (error) {
console.error('[plugins] a listener failed', error);
}
});
}
const pluginApi: IOctanePluginApi = {
register(plugin: IOctanePlugin) {
if (_plugins.some((p) => p.name === plugin.name)) return;
// A plugin is listed once its onInit went through: one that failed used to stay registered half set up.
try {
plugin.onInit?.(pluginApi);
} catch (error) {
console.error(`[plugins] ${plugin.name} failed to start`, error);
return;
}
_plugins.push(plugin);
notifyListeners();
},
unregister(name: string) {
const index = _plugins.findIndex((p) => p.name === name);
if (index >= 0) {
_plugins[index].onClose?.();
_plugins.splice(index, 1);
notifyListeners();
}
},
getPlugins() {
return [..._plugins];
},
createLinkEvent(link: string) {
CreateLinkEvent(link);
},
getRoomEngine() {
return GetRoomEngine();
},
getRoomSession() {
return GetRoomSession();
},
sendMessage: SendMessageComposer,
sendChat(text: string, styleId: number = 0) {
const session = GetRoomSession();
if (!session) return;
session.sendChatMessage(text, styleId, '');
},
sendStackHeight(objectId: number, height: number) {
SendMessageComposer(new FurnitureStackHeightComposer(objectId, height));
},
async takeScreenshot() {
try {
const session = GetRoomSession();
if (!session) return;
const texture = GetRoomEngine().createTextureFromRoom(session.roomId, 1);
if (!texture) return;
const imageUrl = await TextureUtils.generateImageUrl(texture);
texture.destroy(true);
if (!imageUrl) return;
// Download the image
const link = document.createElement('a');
link.href = imageUrl;
link.download = `room_${session.roomId}_${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (e) {
console.warn('[OctanePlugins] Screenshot failed:', e);
}
},
visitDesktop() {
VisitDesktop();
},
createWindow(id: string, title: string, width: number): HTMLDivElement | null {
// Remove existing window with same id
pluginApi.destroyWindow(id);
// Create overlay container
const overlay = document.createElement('div');
overlay.id = `octane-plugin-window-${id}`;
overlay.style.cssText = 'position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)';
// Card wrapper
const card = document.createElement('div');
card.style.cssText = `width:${width}px;background:#2c3e50;border:1px solid #283F5D;border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden;font-family:Ubuntu,sans-serif`;
// Header (draggable)
const header = document.createElement('div');
header.style.cssText =
'display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none';
const titleEl = document.createElement('span');
titleEl.textContent = title;
titleEl.style.cssText = 'color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)';
const closeBtn = document.createElement('div');
closeBtn.style.cssText =
'position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)';
closeBtn.innerHTML = '✕';
closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id));
header.appendChild(titleEl);
header.appendChild(closeBtn);
// Make draggable
let isDragging = false;
let offsetX = 0,
offsetY = 0;
const onMouseDown = (e: MouseEvent) => {
isDragging = true;
const rect = overlay.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
overlay.style.transform = 'none';
overlay.style.left = rect.left + 'px';
overlay.style.top = rect.top + 'px';
};
const onMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
overlay.style.left = e.clientX - offsetX + 'px';
overlay.style.top = e.clientY - offsetY + 'px';
};
const onMouseUp = () => {
isDragging = false;
};
header.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
// Content area
const content = document.createElement('div');
content.style.cssText = 'padding:16px';
card.appendChild(header);
card.appendChild(content);
overlay.appendChild(card);
document.body.appendChild(overlay);
_windowCleanup.set(id, () => {
header.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
});
return content;
},
destroyWindow(id: string) {
const cleanup = _windowCleanup.get(id);
cleanup?.();
_windowCleanup.delete(id);
const existing = document.getElementById(`octane-plugin-window-${id}`);
if (existing) existing.remove();
}
};
/**
* Subscribe to plugin list changes. Returns unsubscribe function.
*/
export function subscribePlugins(listener: () => void): () => void {
_listeners.push(listener);
return () => {
const idx = _listeners.indexOf(listener);
if (idx >= 0) _listeners.splice(idx, 1);
};
}
export function getRegisteredPlugins(): IOctanePlugin[] {
return [..._plugins];
}
// Expose globally so external scripts can use it
(window as any).OctanePlugins = pluginApi;
// Legacy alias for plugins written against the old Nitro global
(window as any).NitroPlugins = pluginApi;
export { pluginApi };
