-
Notifications
You must be signed in to change notification settings - Fork 47
Layout System
The goal of the Cockpit Layout system is to support layout of 2.5D UI elements in a way that can adapt to different spatial topologies. The standard example would be a 2D box layout. However in VR environments we may instead wish to organize UI elements on a cylinder or sphere around the user, or at fixed spatial locations.
At the top level, multiple instances of ILayout can be registered with a Cockpit. The basic strategy is that you construct a Layout, and then add SceneUIElements to it, usually with constraints, and they will be managed automatically.
Layouts are registered with Cockpit.AddLayout, which takes a string name, so that layouts can be referenced by name, ie Cockpit.Layout(Name).Add(..)
Named Layouts are supported to simplify UI management/organization. For example your UI might involve left and right toolbars on top of a 3D scene, and also a set 2D UI elements that track the location of 3D scene points as they move (eg labels or markers). Each of these could be implemented using a different ILayout instance, which uses a different layout strategy. Even with the same layout strategy, regions of the viewport can be assigned to different named layouts.
The other main element of the layout system is ILayoutSolver, which provides the lower-level layout implementation. In fact you can use ILayoutSolver separately from ILayout, for example to do layout of sub-elements of a widget. You also add SceneUIElements to the ILayoutSolver, and then use RecomputeLayout() to position these elements.
We provide a default BaseLayoutSolver implementation which provides the basic object management.
Most built-in UI Elements implement the IBoxModelElement interface, which just means they provide a 2D bounding box. However the BoxModel static class provides many positioning and derivative-box functions based on this bounding box. Much of the built-in UI layout system is based on 2D bounding boxes via IBoxModelElement.
Much of our built-in layout system is designed around the idea that UI layouts are generally 2.5D, but the layout "surface" may not necessarily be a flat rectangle. This is usually the case in traditional desktop and tablet apps, but in VR and AR, UI elements may be organized on a cylinder, sphere, or other surface. (This kind of interface can also sometimes be seen in desktop apps).
Rather than have separate layout implementations for each kind of surface, the ISurfaceBoxRegion interface abstraction represents a rectangular 2D region of a 3D surface. For example a section of a cylinder (CylinderBoxRegion) or sphere (SphereBoxRegion). These implementations provide a 2D bounding box as well as mappings between the 3D surface and the embedded 2D region. The majority of the layout system can work within this embedded 2D region as if it were a traditional 2D rectangular layout space.
(Of course this is not strictly true because of distortion in the mappings (eg on a sphere) but it works well enough for UI layout)
UI Layouts are usually relative to an outer bounding box. We call these Containers, and the ContainerBoundsProvider interface provides a 2D bounding box and a change notification. Two standard implementations are provided. Cockpit2DContainerProvider provides the 2D orthographic-camera window/viewport rectangle, and is meant for use in desktop and tablet apps where the 2D Cockpit is supported. Alternately BoxRegionContainerProvider wraps an ISurfaceBoxRegion and exposes its 2D region (of a 3D surface) as a Container.
The ContainerBoundsProvider implementations can be wrapped in a BoxContainer object. This is a convenience class which forwards their events but also provides an IBoxModelElement interface.
Our built-in UI layout system is based on the metaphor of "pinning" UI elements to eachother. The basic idea is that you can "pin" a point of an objects 2D bounding box to another point, which in most cases will be a point in another 2D bounding box. These points may actually be on 3D surfaces, or determined by projecting points in the 3D scene onto the screen. The layout system is designed to transparently handle these cases.
At the top level the PinnedBoxesLayout implements ILayout, and in most cases this is the only class you will interact with. It contains a PinnedBoxesLayoutSolver instance, which currently is an abstract class that has two implementations, PinnedBoxes2DLayoutSolver for 2D cockpit viewports (ie in a desktop app) and PinnedBoxes3DLayoutSolver for layout onto ISurfaceBoxRegion 3D surfaces (ie onto a cylinder around the viewer in a VR app).
The PinnedBoxesLayout takes a BoxContainer implementation, which provides the "outer bounds" of the layout space. Elements are generally laid out relative to the container bounds, although this is not (currently) strictly enforced, and can be over-ridden with pins.
The "Pins" themselves are simply Func<Vector2f>
function objects, which return the Pin location. The static class LayoutUtil provides utility functions for constructing these function objects based on the IBoxModelElement interface (see example below).
You can go a long way with the built-in PinnedBoxesLayout, however the system is designed to allow for customizations to be plugged in at multiple levels. At the highest level you can write your own ILayout implementation (editing PinnedBoxesLayout is an easy way to start). Or you can subclass PinnedBoxesLayoutSolver to modify or customize the per-object layout behavior. You can also modify BoxContainer or provide your own ContainerBoundsProvider to control the bounding region of the layout. Finally if you want to us the existing layout system with a different 2D/3D mapping, you can simply provide a new ISurfaceBoxRegion implementation.
// create 2D window viewport container
BoxContainer screenContainer = new BoxContainer(new Cockpit2DContainerProvider(cockpit));
// create the layout solver based on the viewport container
PinnedBoxes2DLayoutSolver screenLayout = new PinnedBoxes2DLayoutSolver(screenContainer);
// create and register the cockpit-level layout instance
PinnedBoxesLayout layout = new PinnedBoxesLayout(cockpit, screenLayout);
cockpit.AddLayout(layout, "2D", true);
// create a UI element
HUDLabel label = new HUDLabel(...);
label.Create();
// add to layout, pinning it to the bottom-center of the layout
layout.Add( label, new LayoutOptions() { Flags = LayoutFlags.None,
PinSourcePoint2D = LayoutUtil.BoxPointF(label , BoxPosition.CenterBottom),
PinTargetPoint2D = LayoutUtil.BoxPointF(layout.BoxElement, BoxPosition.CenterBottom)
});
// add button pinned to top-right of label
HUDButton button = new HUDButton (...);
button.Create();
layout.Add( button, new LayoutOptions() { Flags = LayoutFlags.None,
PinSourcePoint2D = LayoutUtil.BoxPointF(button, BoxPosition.BottomLeft),
PinTargetPoint2D = LayoutUtil.BoxPointF(label, BoxPosition.TopRight)
});
Below is the sample code to set up a layout on a cylindrical subregion around the viewer. Note that most of the code is almost identical to the code above. Once the PinnedBoxesLayout is reached, then the exact same code can be used to add objects to the layout. Depending on the interface, you may want to customize the pin constraints for VR vs desktop use. However the UI will be functional even if it is not very attractive.
// create a 2D subregion of a 3D cylinder
CylinderBoxRegionregion3d = new CylinderBoxRegion(...);
// create container for the cylinder region
BoxContainer uiContainer = new BoxContainer(new BoxRegionContainerProvider(cockpit, region3d));
// create the layout solver based on the container
PinnedBoxes3DLayoutSolver layoutCalc = new PinnedBoxes3DLayoutSolver(uiContainer, region3d);
// create and register the cockpit-level layout instance
PinnedBoxesLayout layout = new PinnedBoxesLayout(cockpit, layoutCalc);
cockpit.AddLayout(layout, "Surface", true);
(same code as above to add label and button)