diff --git a/content/ADR-282-inspector.md b/content/ADR-282-inspector.md new file mode 100644 index 00000000..f7188e01 --- /dev/null +++ b/content/ADR-282-inspector.md @@ -0,0 +1,480 @@ +--- +adr: 282 +date: 2025-01-17 +title: Decentraland Inspector +authors: + - cazala +status: Final +type: Standards Track +spdx-license: CC0-1.0 +--- + +# Abstract + +The `@dcl/inspector` package implements a scene editor interface for Decentraland, built on React, BabylonJS, and TypeScript. It provides a modular architecture that can be integrated into different environments through well-defined RPC interfaces and abstractions. + +# Core Components + +## Entity Hierarchy + +A React component that renders and manages the scene's entity tree. It: + +- Uses the ECS SDK to track entity relationships and components +- Manages special entities: `ROOT`, `PLAYER`, `CAMERA` +- Implements entity operations through the SDK components API +- Supports entity icons based on component composition (e.g., smart items, custom items, tiles) +- Uses a generic `Tree` component for rendering hierarchical data + +### Component Inspector + +Handles component editing through specialized inspectors: + +Component-specific inspectors include: + +- `TransformInspector`: Position, rotation, and scale +- `GltfInspector`: 3D model configuration +- `MaterialInspector`: Material properties and textures +- `ActionInspector`: Smart item action configuration +- `TriggerInspector`: Smart item trigger setup +- `StatesInspector`: Smart item state management +- `TextShapeInspector`: Text rendering properties +- `AudioSourceInspector`: Sound configuration +- `VideoPlayerInspector`: Video playback settings +- `NftShapeInspector`: NFT display configuration +- `AnimatorInspector`: Animation controls +- `PointerEventsInspector`: Interaction handling +- `CounterInspector`: Value tracking +- `VisibilityComponentInspector`: Display controls +- `MeshColliderInspector`: Collision properties +- `MeshRendererInspector`: Rendering settings + +A complete list of all available inspectors can be found in the [`EntityInspector`](https://github.com/decentraland/js-sdk-toolchain/tree/main/packages/%40dcl/inspector/src/components/EntityInspector) folder of the SDK repository. + +**Features:** + +- Dynamically loads inspectors based on attached components +- Supports both single and multi-entity selection +- Implements basic/advanced view modes through `Config` component +- Uses React hooks for component state management + +### Level Editor + +3D scene visualization and manipulation: + +- Integrates with the ECS engine for real-time updates +- Implements transform gizmos using Babylon.js (position, rotation, scale) +- Handles drag-and-drop through the DataLayer API +- Manages camera controls and viewport state using Babylon.js scene management +- Uses Babylon.js for 3D rendering and scene manipulation + +### Asset Management + +The Assets panel provides access to three main sources of content: + +1. **Local Assets** + + - Shows project-specific resources (models, textures, sounds) + - Provides direct access to files in the project's directory + - Supports importing new assets from local filesystem + - Manages asset organization within the project structure + +2. **Custom Items** + + - Displays user-created reusable items + - Items can be created by selecting entities in the scene + - Stores items with their complete configuration (components, resources) + - Supports operations like rename, delete, and reuse + - Items can be either static or interactive (smart items) + +3. **Asset Packs** + + - Provides access to official Decentraland items + - Includes both static and smart items + - Organized in themed collections (cyberpunk, fantasy, etc.) + - Items are distributed via the `@dcl/asset-packs` package + - Available through CDN at builder-items.decentraland.org + +Additional functionality includes: + +- Asset import interface for adding new resources +- Asset renaming capabilities +- Custom item creation workflow +- Drag-and-drop support for scene placement + +# Integration Architecture + +## Data Layer RPC + +The Data Layer provides a complete interface for all editor operations, handling both scene state and asset management. It serves as the primary communication channel between the Inspector and its host environment, responsible for: + +1. **Scene State Management:** + + - Maintains scene entity hierarchy and component data + - Synchronizes state changes through CRDT streaming + - Handles undo/redo operations + - Manages scene saving and loading + +2. **Asset Management:** + - Creates and modifies custom items + - Manages the asset catalog + - Handles asset metadata and thumbnails + - Controls asset lifecycle (create, rename, delete) + +The Data Layer has two implementations: + +1. **Local (In-Memory):** + + - Used for development and testing + - Stores all state in memory + - No network communication required + - Fast and suitable for local development + +2. **Remote (Protobuf):** + - Used in production environments + - Communicates over network using protobuf messages + - Supports WebSocket transport + - Enables integration with remote services + +The remote Data Layer is implemented as a protobuf-defined RPC service to ensure type safety and versioning: + +```protobuf +service DataService { + // Scene state synchronization + rpc CrdtStream(stream CrdtStreamMessage) returns (stream CrdtStreamMessage) + + // Asset management + rpc CreateCustomAsset(CreateCustomAssetRequest) returns (CreateCustomAssetResponse) + rpc GetCustomAssets(Empty) returns (GetCustomAssetsResponse) + rpc GetAssetCatalog(Empty) returns (AssetCatalogResponse) + rpc DeleteCustomAsset(DeleteCustomAssetRequest) returns (Empty) + rpc RenameCustomAsset(RenameCustomAssetRequest) returns (Empty) + + // File operations + rpc GetFiles(GetFilesRequest) returns (GetFilesResponse) + rpc SaveFile(SaveFileRequest) returns (Empty) + rpc GetFile(GetFileRequest) returns (GetFileResponse) + rpc CopyFile(CopyFileRequest) returns (Empty) + + // Editor state + rpc Undo(Empty) returns (UndoRedoResponse) + rpc Redo(Empty) returns (UndoRedoResponse) + rpc Save(Empty) returns (Empty) +} +``` + +### File System Interface + +The File System Interface is a lower-level abstraction focused solely on file operations. It has three distinct implementations: + +1. In-Memory ([`feeded-local-fs.ts`](https://github.com/decentraland/js-sdk-toolchain/tree/main/packages/@dcl/inspector/src/lib/data-layer/client/feeded-local-fs.ts)): + + - Uses Map data structures to store files in memory + - Simulates a complete file system + - Used for development and testing + - No persistence between sessions + +2. Node.js ([`sdk-commands/start/data-layer/fs.ts`](https://github.com/decentraland/js-sdk-toolchain/blob/main/packages/%40dcl/sdk-commands/src/commands/start/data-layer/fs.ts)): + + - Direct implementation using Node.js `fs` module + - Used by the CLI for local development + - Handles file watching and hot reloading + - Provides real filesystem access + +3. IFrame ([`iframe-storage.ts`](https://github.com/decentraland/js-sdk-toolchain/tree/main/packages/@dcl/inspector/src/lib/logic/storage/iframe.ts)): + - Uses mini-rpc library for communication + - Delegates file operations to parent window through postMessage + - Parent window implements actual storage: + - Web Editor: Uses Builder Server API + - Creators Hub: Uses Electron's Node.js `fs` module + - Enables browser and desktop support through abstraction + +The interface definition remains intentionally simple: + +```typescript +export type FileSystemInterface = { + dirname: (path: string) => string; + basename: (filePath: string) => string; + join: (...paths: string[]) => string; + existFile: (filePath: string) => Promise; + readFile: (filePath: string) => Promise; + writeFile: (filePath: string, content: Buffer) => Promise; + readdir: ( + dirPath: string + ) => Promise<{ name: string; isDirectory: boolean }[]>; + rm: (filePath: string) => Promise; + cwd: () => string; +}; +``` + +### Additional RPCs + +All additional RPCs are implemented using the [`@dcl/mini-rpc`](https://github.com/decentraland/mini-rpc) library, which provides type-safe client/server communication. These include: + +1. Storage RPC: Implements the IFrame file system interface +2. Camera RPC: Controls viewport and screenshots +3. UI RPC: Manages Inspector UI state +4. Scene Metrics RPC: Reports scene statistics + +Each RPC uses postMessage for transport in IFrame implementations and follows the mini-rpc pattern of: + +- Type-safe method definitions +- Client/server architecture +- Event emission capabilities + +### Relationship Between Layers + +The Data Layer uses the File System Interface but adds: + +- Scene-specific operations (CRDT streaming, undo/redo) +- Asset management (custom items, catalogs) +- Editor state management +- Higher-level abstractions for scene editing + +Example: + +- When creating a custom item, the Data Layer: + 1. Handles the item metadata and composition + 2. Uses the File System Interface to store resources + 3. Manages thumbnails and previews + 4. Updates the asset catalog + +While the File System Interface would only handle the raw file operations without understanding the asset structure or scene context. + +# Integration Types + +## IFrame Integration + +The parent application embeds the Inspector in an IFrame and communicates through postMessage: + +### Storage RPC Setup + +The parent application needs to set up the RPC bridge to handle file system operations: + +```typescript +function initRpc(iframe: HTMLIFrameElement) { + const transport = new MessageTransport(window, iframe.contentWindow!); + const storage = new StorageRPC(transport); + + // Handle file operations + storage.handle("read_file", async ({ path }) => { + return fs.readFile(path); + }); + + storage.handle("write_file", async ({ path, content }) => { + await fs.writeFile(path, content); + }); + + storage.handle("exists", async ({ path }) => { + return fs.exists(path); + }); + + storage.handle("delete", async ({ path }) => { + await fs.rm(path); + }); + + storage.handle("list", async ({ path }) => { + const files = await fs.readdir(path); + return Promise.all( + files.map(async (name) => ({ + name, + isDirectory: await fs.isDirectory(path.join(path, name)), + })) + ); + }); + + return { + storage, + dispose: () => storage.dispose(), + }; +} +``` + +### React Component Integration + +Example of embedding the Inspector in a React application: + +```typescript +const CONTENT_URL = "http://localhost:3000"; // URL to your iframe content + +function InspectorComponent() { + const iframeRef = useRef(); + + const handleIframeRef = useCallback((iframe) => { + if (iframe) { + iframeRef.current = initRpc(iframe); + } + }, []); + + useEffect(() => { + return () => iframeRef.current?.dispose(); + }, []); + + const params = new URLSearchParams({ + dataLayerRpcParentUrl: window.location.origin, // this is the url of the parent application + }); + const url = `${CONTENT_URL}?${params}`; // url where the inspector is being served + + return