Specialized components
UI framework
Editor properties UI (NodeEditor components)
15 min
overview the editor properties ui system provides a structured framework for creating and managing property editing panels within the ir engine editor these panels allow users to inspect and modify the properties of entities in the scene without writing code built on top of the ui primitives discussed in the previous chapter, this system organizes related properties into collapsible sections, each dedicated to a specific component type this approach creates an intuitive and consistent editing experience throughout the application core concepts entity component system (ecs) the ir engine uses an entity component system architecture where entities are the objects in the scene (e g , a lamp, a character, a camera) components are data containers attached to entities (e g , transformcomponent , lightcomponent ) systems process entities with specific components the editor properties ui focuses on providing interfaces for editing the data stored in components nodeeditor components nodeeditor components are specialized react components that create editing interfaces for specific component types each nodeeditor retrieves the component data for the selected entity renders appropriate input controls for each property updates the component data when users modify values provides a consistent visual structure with collapsible sections these components serve as the bridge between the ui and the underlying data model, allowing users to modify entity properties through a graphical interface implementation nodeeditor structure a typical nodeeditor component follows this structure // simplified from src/components/editor/properties/light/ambient/index tsx import nodeeditor from '@ir engine/editor/src/panels/properties/common/nodeeditor'; import { ambientlightcomponent } from '@ir engine/spatial'; import { usecomponent } from '@ir engine/ecs'; import { colorinput, numericinput, inputgroup } from '@ir engine/ui'; import { hioutlinesun } from 'react icons/hi2'; export const ambientlightnodeeditor = (props) => { // get the component data for the selected entity const lightcomponent = usecomponent(props entity, ambientlightcomponent); return ( \<nodeeditor name="ambient light" description="sets the global ambient light properties " icon={hioutlinesun} entity={props entity} \> \<inputgroup name="color" label="light color"> \<colorinput value={lightcomponent color value} onchange={updateproperty(ambientlightcomponent, 'color')} onrelease={commitproperty(ambientlightcomponent, 'color')} /> \</inputgroup> \<inputgroup name="intensity" label="light intensity"> \<numericinput value={lightcomponent intensity value} onchange={updateproperty(ambientlightcomponent, 'intensity')} onrelease={commitproperty(ambientlightcomponent, 'intensity')} /> \</inputgroup> \</nodeeditor> ); }; this component uses the usecomponent hook to access the ambientlightcomponent data wraps everything in a nodeeditor container that provides the collapsible section creates input fields for each property ( color and intensity ) connects the input fields to the component data using updateproperty and commitproperty common nodeeditor wrapper the nodeeditor wrapper component provides a consistent visual structure for all property editors // simplified concept from @ir engine/editor/src/panels/properties/common/nodeeditor import react, { usestate } from 'react'; import { twmerge } from 'tailwind merge'; const nodeeditor = ({ name, description, icon, children, defaultexpanded = true, props }) => { const \[expanded, setexpanded] = usestate(defaultexpanded); return ( \<div classname="mb 2"> {/ header section with icon, name, and expand/collapse control /} \<div classname="flex items center p 2 bg ui secondary rounded t cursor pointer" onclick={() => setexpanded(!expanded)} \> {icon && \<icon classname="mr 2 text ui primary" />} \<div classname="flex 1"> \<h3 classname="text sm font medium">{name}\</h3> {description && \<p classname="text xs text text secondary">{description}\</p>} \</div> \<div classname="transform transition transform"> {expanded ? '▼' '►'} \</div> \</div> {/ content section that shows/hides based on expanded state /} {expanded && ( \<div classname="p 2 border border ui outline rounded b"> {children} \</div> )} \</div> ); }; export default nodeeditor; this wrapper manages the expanded/collapsed state of the section renders a header with an icon, name, and description provides a toggle control for expanding/collapsing conditionally renders the content (input fields) based on the expanded state componentdropdown some nodeeditors use a more specific componentdropdown component that extends the basic nodeeditor pattern // simplified from src/components/editor/properties/transform/index tsx import componentdropdown from ' / /componentdropdown'; import { transformcomponent } from '@ir engine/spatial'; import { vector3input, eulerinput } from '@ir engine/ui'; import { lumove3d } from 'react icons/lu'; export const transformpropertygroup = (props) => { const transformcomponent = usecomponent(props entity, transformcomponent); return ( \<componentdropdown name="transform" description="position, rotation, and scale of the object " icon={lumove3d} entity={props entity} \> \<inputgroup name="position" label="position"> \<vector3input value={transformcomponent position value} onchange={updateproperty(transformcomponent, 'position')} onrelease={commitproperty(transformcomponent, 'position')} /> \</inputgroup> \<inputgroup name="rotation" label="rotation"> \<eulerinput value={transformcomponent rotation value} onchange={updateproperty(transformcomponent, 'rotation')} onrelease={commitproperty(transformcomponent, 'rotation')} /> \</inputgroup> \<inputgroup name="scale" label="scale"> \<vector3input value={transformcomponent scale value} onchange={updateproperty(transformcomponent, 'scale')} onrelease={commitproperty(transformcomponent, 'scale')} /> \</inputgroup> \</componentdropdown> ); }; the componentdropdown typically provides additional functionality specific to component management, such as component addition/removal controls component specific actions integration with the component registry property update functions the updateproperty and commitproperty functions handle the connection between ui inputs and component data // simplified concept from @ir engine/editor export const updateproperty = (componenttype, propertyname) => (newvalue) => { // get the currently selected entity const entity = getselectedentity(); // get the component instance const component = getcomponent(entity, componenttype); // update the property value (may be staged for undo/redo) component\[propertyname] set(newvalue); // mark the component as modified markcomponentmodified(entity, componenttype); }; export const commitproperty = (componenttype, propertyname) => (finalvalue) => { // get the currently selected entity const entity = getselectedentity(); // finalize the change (e g , add to undo/redo history) commitcomponentchange(entity, componenttype, propertyname, finalvalue); }; these functions create handlers that know which component type and property to update handle the details of accessing and modifying component data integrate with the editor's undo/redo system ensure changes are properly propagated to the underlying systems inspector workflow the process of displaying and editing properties follows this workflow sequencediagram participant user participant inspector as inspector panel participant entity as selected entity participant registry as nodeeditor registry participant nodeeditor as component nodeeditor participant component as ecs component user >>inspector selects an entity inspector >>entity get attached components entity >>inspector returns component list loop for each component inspector >>registry find nodeeditor for component type registry >>inspector returns appropriate nodeeditor inspector >>nodeeditor render for this entity nodeeditor >>component get current values component >>nodeeditor returns property values nodeeditor >>inspector renders editing interface end inspector >>user displays property panels user >>nodeeditor modifies a property nodeeditor >>component updateproperty() component >>nodeeditor updates ui with new value user >>nodeeditor completes edit (blur/enter) nodeeditor >>component commitproperty() component >>component finalizes change nodeeditor examples the ir engine includes nodeeditors for various component types camera properties // simplified from src/components/editor/properties/camera/index tsx export const cameranodeeditor = (props) => { const component = usecomponent(props entity, cameracomponent); return ( \<nodeeditor name="camera" description="camera properties " icon={hioutlinecamera} { props} \> \<inputgroup name="field of view (fov)" label="fov"> \<numericinput value={component fov value} onchange={updateproperty(cameracomponent, 'fov')} onrelease={commitproperty(cameracomponent, 'fov')} min={1} max={179} /> \</inputgroup> \<inputgroup name="near clip" label="near clip"> \<numericinput value={component near value} onchange={updateproperty(cameracomponent, 'near')} onrelease={commitproperty(cameracomponent, 'near')} min={0 001} /> \</inputgroup> \<inputgroup name="far clip" label="far clip"> \<numericinput value={component far value} onchange={updateproperty(cameracomponent, 'far')} onchange={updateproperty(cameracomponent, 'far')} min={0 1} /> \</inputgroup> \</nodeeditor> ); }; collider properties // simplified from src/components/editor/properties/collider/index tsx export const collidernodeeditor = (props) => { const component = usecomponent(props entity, collidercomponent); return ( \<nodeeditor name="collider" description="physical collision properties " icon={tbcubesend} { props} \> \<inputgroup name="shape" label="collision shape"> \<select value={component shape value} onchange={updateproperty(collidercomponent, 'shape')} options={\[ { value 'box', label 'box' }, { value 'sphere', label 'sphere' }, { value 'capsule', label 'capsule' } ]} /> \</inputgroup> {component shape value === 'box' && ( \<inputgroup name="size" label="box size"> \<vector3input value={component boxsize value} onchange={updateproperty(collidercomponent, 'boxsize')} onrelease={commitproperty(collidercomponent, 'boxsize')} /> \</inputgroup> )} {component shape value === 'sphere' && ( \<inputgroup name="radius" label="sphere radius"> \<numericinput value={component radius value} onchange={updateproperty(collidercomponent, 'radius')} onrelease={commitproperty(collidercomponent, 'radius')} min={0 01} /> \</inputgroup> )} {/ additional shape specific properties /} \</nodeeditor> ); }; next steps with an understanding of how property editing panels are structured, the next chapter explores the specialized input components that make these panels powerful and intuitive these components provide tailored interfaces for specific data types like vectors, colors, and 3d models next specialized editor input components docid\ isa7tlc5s3xtzrmikavqq