Help
  • Explore Community
  • Get Started
  • Ask the Community
  • How-To & Best Practices
  • Contact Support
Notifications
Login / Register
Schneider Electric
Community
Community
Notifications
close
  • Categories
  • Forums
  • Knowledge Center
  • Blogs
  • Ideas
  • Events & Webinars
Help
Help
  • Explore Community
  • Get Started
  • Ask the Community
  • How-To & Best Practices
  • Contact Support
Login / Register

Contact Support

Close

Ask our Experts

Have a question related to our products, solutions or services? Get quick support on community Forums

Email Us

For Community platform-related support, please email us

New Community Ranking System
Our Community ranking system has recently been updated. You may notice changes in user rankings and receive system messages or notifications. If you have questions about how the new ranking works, please refer to the announcement post for more details (click here).

Digital Twin

This knowledge base is addressing usage of software Experior, Machine Expert Twin and future Automation Expert Twin. These softwares are used to create smaller instances of digital twins for use in industrial automation, warehouse management, design & engineering with more..

Search in

Improve your search experience:

  • Exact phrase → Use quotes " " (e.g., "error 404")
  • Wildcard → Use * for partial words (e.g., build*, *tion)
  • AND / OR → Combine keywords (e.g., login AND error, login OR sign‑in)
  • Keep it short → Use 2–3 relevant words , not full sentences
  • Filters → Narrow results by section (Knowledge Base, Users, Products)
cancel
Turn on suggestions
Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Select a Country

Please select a country to continue with beta search.

  • Home
  • Schneider Electric Community
  • Knowledge Center
  • Digital Twin
  • Digital Twin
  • Label: Exp-6 Developer Guide
Options
  • Knowledge Base Article Dashboard
  • Subscribe
  • Bookmark
  • Invite a Friend
Invite a Co-worker
Send a co-worker an invite to the portal.Just enter their email address and we'll connect them to register. After joining, they will belong to the same company.
You have entered an invalid email address. Please re-enter the email address.
This co-worker has already been invited to the Exchange portal. Please invite another co-worker.
Please enter email address
Send Invite Cancel
Invitation Sent
Your invitation was sent.Thanks for sharing Exchange with your co-worker.
Send New Invite Close
Labels
Top Labels
  • Alphabetical
  • Experior 6 85
  • Experior 7 57
  • Exp-6 Developer Guide 35
  • Exp-7 Developer 16
  • Exp-6 User Guides 16
  • Exp-7 Communication Protocols 11
  • Exp-6 Experior 11
  • Exp-6 Communication Protocols 11
  • Exp-7 Getting Started 10
  • Exp-6 Dev Environment 9
  • Exp-7 User Interface 9
  • Exp-6 Dev Assembly 7
  • Exp-7 Building Models 6
  • Exp-7 Working With Models 5
  • Exp-6 PLC 4
  • Exp-6 Getting Started 3
  • Exp-7 Importing Graphics 3
  • Exp-6 3rd Party Programs 2
  • Exp-6 Tips and Tricks 2
  • Remote Viewer 2
  • Exp-6 Model Modifications Examples 1
  • Previous
  • 1 of 3
  • Next
Top Contributors
  • Kasper.Vestrup
    Kasper.Vestrup
See More Contributors

Related Forums

  • Intelligent Devices Forum

Previous Next

Invite a Colleague

Found this content useful? Share it with a Colleague!

Invite a Colleague Invite

Label: "exp-6 developer guide"

View in: "Digital Twin" | Community

35 Posts | First Used: 2026-01-09

Digital Twin

Sort by:
Date
  • Date
  • Views
  • Likes
Options
  • Knowledge Base Article Dashboard
  • Subscribe
  • Bookmark
  • Invite a Friend
  • « Previous
    • 1
    • 2
  • Next »
Label: "Exp-6 Developer Guide" Show all articles

Custom connection

It is possible to extend the functionality of Experior with custom communication protocols. This is done by creating your own plugin and extending Experior.Core.Communication.PLC.Connection.   In case your connection is TCP/IP based you can extend from Experior.Core.Communication.PLC.TCPIP.Connection.   Experior.Core.Communication.PLC.Connection In attached memcom communication plugin a custom connection is made that doesn’t use the network to communicate but instead uses a memory stream. This plugin can be used to quickly test your Input/Output driven components. You do this by connecting an input and an output through the same address of the memcom connection.   For example link the Pushed PLC Input symbol of a button to Byte 1 Bit 0 of the memcom connection and also connect the Lighting PLC output to Byte 1 Bit 0 of the memcom connection. When pressing the button the lamp will light up:   The custom connection uses a memory stream to achieve this because a memory stream has similar behavior as a regular network stream.   The source code contains some notes explaining how to achieve the same functionality without the memory stream (and just share the data buffer).   Whenever the user assigns an input/output from a component to a PLC connection then this input/output is assigned to a PLC Buffer (Experior.Core.Communication.PLC.Buffers.Input/Experior.Core.Communication.PLC.Buffers.Output). These buffers maintain an array of bytes and grows/shrinks whenever new inputs/outputs are added/removed.   So in the constructor of the connection we create the buffers and subscribe to the events that are triggered when their size should change or when new inputs/outputs are assigned to them.   It is possible to have multiple buffers in a connection. Therefore we distinguish between them in a connection using a unique key (string) the Source.   In the example plugin only 1 Input buffer and 1 Output buffer is used, each with a default Source name equal to “0”.   In the constructor we also restore the saved buffer in case it exists. public MemConnection(MemConnectionInfo info) : base(info) { input = new Core.Communication.PLC.Buffers.Input(); // first restore the saved outputbuffer if it exists if (info.outputs != null && info.outputs.Count > 0) { output = info.outputs[outslot]; } else output = new Core.Communication.PLC.Buffers.Output(); //give the input and output buffer a unique slot name input.Source = inpslot; output.Source = outslot; // subscribe to the different events to react upon data changes or to properly allocate the buffers output.BufferChanged += new Core.Communication.PLC.Buffers.Output.BufferChangedEvent(OutputDataUpdated); input.BufferChanged += new Core.Communication.PLC.Buffers.Input.BufferChangedEvent(InputDataUpdated); Experior.Core.Communication.PLC.Connection.Inputs.LengthChanged += new Inputs.LengthChangedEvent(Inputs_LengthChanged); Experior.Core.Communication.PLC.Connection.Outputs.LengthChanged += new Outputs.LengthChangedEvent(Outputs_LengthChanged); Experior.Core.Communication.PLC.Connection.Outputs.ConnectionedAssigned += new Outputs.OutputEvent(Outputs_ConnectionedAssigned); Experior.Core.Communication.PLC.Connection.Outputs.ConnectionedUnAssigned += new Outputs.OutputEvent(Outputs_ConnectionedUnAssigned); Experior.Core.Communication.PLC.Connection.Inputs.ConnectionedAssigned += new Inputs.InputEvent(Inputs_ConnectionedAssigned); Experior.Core.Communication.PLC.Connection.Inputs.ConnectionedUnAssigned += new Inputs.InputEvent(Inputs_ConnectionedUnAssigned); // add sources to the connection AddSource(input); AddSource(output); Experior.Core.Environment.Scene.Loaded += new Core.Environment.Scene.Event(Scene_Loaded); }   When the user assigns an Input/Output to the connection it triggers the Experior.Core.Communication.PLC.Connection.Inputs.ConnectionedAssigned and  Experior.Core.Communication.PLC.Connection.Outputs.ConnectionedAssigned events.   In the example we only use this to set the Source property of the Input/Output private void Inputs_ConnectionedAssigned(Input sender, Core.Communication.PLC.Connection connection) { if (this != connection) return; // here you can react upon the sender being added to this connection // in our case we just want to ensure the proper Source is set sender.Source = inpslot; }   Due to the nature of the example connection it is crucial that the Input and Output buffer have the same size so a big part of the source code is related to this.   When assigning Inputs/Outputs to a connection Experior verifies whether the buffer size should change and in those cases triggers the Experior.Core.Communication.PLC.Connection.Inputs.LengthChanged or Experior.Core.Communication.PLC.Connection.Outputs.LengthChanged events.   At all times you ask the minimum and maximum used byte of a input/output buffer by providing the connection and the Source of the buffer: int max = Experior.Core.Communication.PLC.Connection.Inputs.MaxSize(this, inpslot); int offset = Experior.Core.Communication.PLC.Connection.Inputs.MinSize(this, inpslot);   If the buffer is not large enough anymore you can allocate sufficient bytes: // allocate (max-offset) bytes for the input buffer starting from offset input.Allocate(max - offset, offset, Experior.Core.Communication.PLC.Buffers.Buffer.Units.SINT);   Note: Experior.Core.Communication.PLC.Buffers.Buffer.Units.INT is used to indicate that the given size should be measured in bytes. In case Experior.Core.Communication.PLC.Buffers.Buffer.Units.SINT is used the size will be measured in words of 16 bits.   After the allocation the input.Data property will be an array of bytes with the proper size. When the user tries to connect the connection the EstablishConnection() method is called. When he tries to disconnect the Disconnect() method is called. In our example we override both methods and set the proper state of the connection (Core.Communication.State). When establishing the connection we make sure that the Input/Output buffers are allocated with the same size. In case we use a memorystream we also start listening on the stream (similar as we would be on a networkstream).   When the states of the Input/Outputs are changed this is immediately reflected in changes of the corresponding Input/Output buffers of the associated connection and the BufferChanged events are triggered. In our example we write the changed buffer to the memorystream. A different thread reads the memory stream and when the byte array is received it calls the byte[] DataReceived(string source, byte[] data) method of the connection to indicate that the given source has received the given data. int l = memStream.Read(incoming, 0, incoming.Length); if (l > 0) { DataReceived(inpslot, incoming); }   This triggers the proper events on the changed Inputs/Outputs. In the example without the memorystream we simply share the Data of the buffer and directly call the DataReceived method.   Experior.Core.Communication.PLC.TCPIP.Connection This connection class inherits from Experior.Core.Communication.PLC.Connection and mainly adds some TCPIP specific properties like Port, IP address and distinction between server and client connection: string IP: This getter property is used to get / or set the IPv4 address used by this connection. For a server connection this is 127.0.0.1 (localhost).   int Port: This property is used to get or set the port number for the TCPIP connection.   bool Server: Getter property to indicate whether this connection is used as server or client.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:57 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
266 Views

ActionPoint class

Action Points are points on a Route where a load can stop and stay. They are used internally by Experior as vertices or nodes in the routing graph (where parts of the route between the action points are the edges).   They are implemented in Experior using the Experior.Core.Routes.ActionPoint class.   Main properties: string Name: Getter property for the name of this actionpoint. string APName: Gets or sets the name of this actionpoint. Route Parent: This getter property returns the route to which this actionpoint is added. Assembly:  This getter property returns the Assembly object to which this actionpoint belongs. Vector3 Position: This getter property returns the global position of this actionpoint. bool Routing: Gets or sets a value indicating whether this actionpoint should be included as node in the routing graph. bool Selectable: Gets or sets a value indicating whether this actionpoint is pickable by the user. bool Selected: Gets or sets a value indicating whether this actionpoint is selected. Experior.Core.Reports.Statistics.Statistic.Counter Statistic: This getter property returns the counter statistic linked to this actionpoint. ActionPoint.StoppingMode StopMode: This property gets or sets the stopping mode of this actionpoint. Possible values are: StoppingMode.None, StoppingMode.Stop, StoppingMode.Capacity, StoppingMode.StopMotor and only available in discrete event mode StoppingMode.Interval. ActionPoint.Edges Collision: This property gets/sets when an actionpoint triggers its Enter event. float Distance: This property gets/sets the distance of this actionpoint on its route measured from the start of the route. bool Active: This getter property returns whether this actionpoint is active (has load) or not. Load ActiveLoad: This getter property returns whether the current load on this actionpoint (or null in case the actionpoint is not active). string VirtualNode: This property gets/sets whether this actionpoint is a virtual node or not (default). This VirtualNode is used to join a group of nodes into one edge in the route graph. All actionpoints with the same VirtualNode value will behave as 1 node in the routing graph. static ReadOnlyDictionary<string, ActionPoint> Items: Static getter property that returns a read-only dictionary with all existing user actionpoints. Key in the dictionary is the name of the actionpoint while the value is the actual ActionPoint instance. Note: this dictionary only contains actionpoints that were added by the user, it does not contain the actionpoints that were created by code internally in assemblies. The developer should keep track of those.   Main events: event ActionPoint.EnterEvent Enter: This event is raised when a load enters this actionpoint. event ActionPoint.ReleasedEvent Released: This event is raised when a load leaves this actionpoint. It can be because the load was never stopped, or because the load is deleted or released or  switched to another ActionPoint or Route. event ActionPoint.MoveToEvent MoveTo: This event is raised when the MoveTo method is called on the load. event ActionPoint.RemovedEvent Removed: This event is raised when an actionpoint is deleted.   Example: public ActionPoint CreateActionPoint (int index) { ActionPoint ap = new ActionPoint(); ap.Edge = ActionPoint.Edges.Leading; ap.StopMode = ActionPoint.StoppingMode.Stop; ap.Name = index.ToString(); ap.Enter += new ActionPoint.EnterEvent(ap_Enter); ap.Released += new ActionPoint.ReleasedEvent(ap_Released); return ap; } void ap_Enter(ActionPoint ap, Load load) { // Example of gathering and logging some routing info // we will lo the previuous and next actionpoint and the distance to them float nextdistance; float prevdistance; ActionPoint next = Route.FindNextActionPoint(ap, out nextdistance); ActionPoint prev = Route.FindPreviousActionPoint(ap, out prevdistance); if (next != null) Experior.Core.Environment.Log.Write("Next ActionPoint of " + ap.Name + " is " + next.Name + " at distance " + nextdistance); else Experior.Core.Environment.Log.Write("No next ActionPoint found for " + ap.Name ); if (prev != null) Experior.Core.Environment.Log.Write("Previous ActionPoint of " + ap.Name + " is " + prev.Name + " at distance " + prevdistance); else Experior.Core.Environment.Log.Write("No previous ActionPoint found for " + ap.Name); // example of some diverting decision based on load info if (load.Identification == ap.Name) { //divert load to route Int16 idx=0; if (Int16.TryParse(ap.Name, out idx)) { load.Switch(divertroutes[idx].Route); load.Release(); } } else load.Release(); } void ap_Released(ActionPoint ap, Load load) { Experior.Core.Environment.Log.Write("ActionPoint " + ap.APName + " is released by load " + load.Identification); }   Main static methods static string GetValidName(string prefix): Actionpoints require a unique name. This method will create a unique name with the given prefix. static ActionPoint Get(string name): A factory method to create and return an actionpoint with the given name.If there was already an actionpoint with the given name then this existing one is returned. static ActionPoint Release(string name): This static method will release the actionpoint with the given name.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:48 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
112 Views

Route class

Routes are used in Experior to move loads following a track (linear, curve) without usage of the physics engine. Therefore routes are mainly used in discrete event mode. Routes are implemented in Experior using the abstract base class Experior.Core.Routes.Route. A straight route is implemented by the Experior.Core.Routes.Linear class, while a curved route is implemented by the Experior.Core.Routes.Curve class.   Routes may contain Action Points which typically represent destination points or decision points. By connecting routes Experior calculates the routing graph based on the nodes (action points) in the network. This routing graph can be used to calculate the shortest path to a new destination (Action Point ).   To incorporate routes in the working area and other components they are usually encapsulated in a TransportSection assembly. The TransportSection assemblies themselves are used as building blocks in the different Track assemblies.   A Route keeps track of the loads that are moving on it and of the Action Points it contains.   Note: In discrete event mode loads are always on a Route or on an Action Point. (Loads that are Deletable will be automatically deleted by Experior when they are not on a Route or Action Point).   Main properties: List ActionPoints: Getter property that returns all action points on this route.   LinkedList Loads: Getter property that returns all loads on this route.   Load First: Getter property returning the first load on this route.   Load Last: Getter property returning the last load on this route.   bool Bidirectional: Property that indicates whether Experior should consider this route as bidirectional or not when constructing the routing graph. Suppose the route between action points AP1 and AP2 is bidirectional than this means that the routing graph will have two edges between the vertices AP1 and AP2. In case the bidirectional property is false only 1 directed edge from AP1 to AP2 is available. Setting the bidirectional property has no impact on the actual movement of the loads: the developer still has to ensure that a load can move in opposite direction.   Motor Motor: Property that gets/sets the Motor used to control the movements of the loads on this Route. Changing the speed of the motor will change the speed by which loads move on its route and changing the speed sign will make the loads move in opposite direction.   Vector3 Start: Property that gets/sets the global position of the startpoint of the route.   Vector3 End: Property that gets/sets the global position of the endpoint of the route.   float Yaw: Getter property that returns the global Yaw of this route.   float Length: Property that gets/sets the total length of this route   bool Vertical: Property that gets or sets whether the route is vertical or not. This has an impact on how loads are positioned on the route.   bool Visible: Gets or sets a value indicating whether this Experior.Core.Routes.Route is visible.   float Yaw: Getter property that returns the global Yaw of this route   Main events: event Route.ArrivedEvent Arrived: This event is raised when a load arrives. This is not synchronized with the discrete event execution.   event Route.LoadAddedEvent LoadAdded: This event is raised when a load is added to the route.   event Route.LoadRemovedEvent LoadRemoved: This event is raised when a load is removed from the route.   static event Route.UpdatedEvent Updated: This event is raised when the routing graph is updated.   Main methods: void Add(Load load): This method will add the given load to the start of this Route.   void Add(Load load, float distance): This method will add the given load to this Route at the given distance from the start.   void Remove(Load load): This method will remove the given load from this Route.   ActionPoint InsertActionPoint(float distance): This method will create and return an actionpoint and position it at the given distance from the start of this route.   void RemoveActionPoint(ActionPoint ap): This method will remove the given actionpoint ap from this route.   void ClearLoads(): This method will remove all loads from this route.   Main static methods: static ActionPoint FindNextActionPoint(ActionPoint ap): Given an actionpoint ap this method will return the next axtionpoint on the route (returns null if no actionpoint is found).   static ActionPoint FindNextActionPoint(Route route): Given a Route route this static method will return the first actionpoint on that route.   static ActionPoint FindNextActionPoint(ActionPoint ap, out float distance): Given an actionpoint ap this static method will return the next actionpoint on the route (if any) and put the distance between the two actionpoints in the distance out parameter. static ActionPoint FindPreviousActionPoint(ActionPoint ap, out float distance): Given an actionpoint ap this static method will return the previous actionpoint on the route of ap and put the distance between the two actionpoints in the distance out parameter. If the route of ap does not have a previous actionpoint then the previous route is looked at to find the previous actionpoint.   static float ShortestDistanceToActionPoint(Load load, string destination): Given a load and the name of an actionpoint this static method will return shortest distance from the current position of the load to the actionpoint by following the shortest path on the linked routes. If no path can be found float.PositiveInfinity is returned and a warning is logged.   static float ShortestDistanceToActionPoint(string sourceAP, string destinationAP): Given the name of a source actionpoint  and the name of a destination actionpoint this static method will return shortest distance between the two actionpoints by following the shortest path on the linked routes. If no path can be found float.PositiveInfinity is returned and a warning is logged.   static void Update(): This static method will result in the updating of the routing graph. (This is also called when a user deletes or adds an actionpoint)   Example: float distance = Route.ShortestDistanceToActionPoint(load, "AP3"); if (float.IsInfinity(distance)) { Experior.Core.Environment.Log.Write("No path found from load " + load.Identification + " to actionpoint AP3"); } else { Experior.Core.Environment.Log.Write("Distance from load " + load.Identification + "to actionpoint AP3=" + distance); } distance = Route.ShortestDistanceToActionPoint("AP2", "AP3"); if (float.IsInfinity(distance)) { Experior.Core.Environment.Log.Write("No path found between AP2 and AP3"); } else { Experior.Core.Environment.Log.Write("Distance from AP2 to AP3=" + distance); }
View full article
Kasper.Vestrup Explorer
‎2026-01-15 01:02 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
153 Views

Property filter

Property Filters are described here as seen from the users point of view.   The configuration files are created the first time the catalog is loaded into Experior.   It is possible to set some predefined filters by using the attribute called Experior.Core.Properties.DefaultFilter.   The code below shows the “Blocked” property for the PhotoEye:
View full article
Kasper.Vestrup Explorer
‎2026-01-14 06:23 AM

Labels:
  • Exp-6 Dev Assembly
  • Exp-6 Developer Guide
  • Experior 6
176 Views

AssemblyInfo class

The AssemblyInfo class is the base class (deriving from EntityInfo) used to manage all data of an Assembly object that needs to be saved. Each constructor of an Assembly class contains an AssemblyInfo object as argument. If the AssemblyInfo class doesn’t contain all necessary fields to restore an assembly then a new class is used that derives from AssemblyInfo. The developer just has to add new public fields to that info class so that this data can be retrieved by the matching assembly constructor.   When saving the model Experior will serialize all AssemblyInfo objects into an Assemblies.XML file and compress it into the Experior model file.   When loading the model the process is reversed: Experior unzips the model file and de-serializes the Assemblies.XML file. Each newly created AssemblyInfo object is passed into the constructor of the matching Assembly class (which is derived from the fullname field).   Due to the nature and purpose of the AssemblyInfo objects they have an empty constructor and most fields are public.   When selecting an icon of the assembly in the catalog window then some class-properties are shown. They contain default values that will be used to create the new Assembly when you drag it onto the working area. By changing those default property values subsequent assemblies will be created with those new values.   Below is described some common fields of each AssemblyInfo object and explain mechanism of storing default properties and illustrate it with an example.   Main fields: public string name: The saved name of the corresponding assembly public string fullname: The saved fully qualifying  class name of the corresponding assembly public string section: The saved section name of the corresponding assembly public Vector3 position: The saved global position of the corresponding assembly public Matrix orientation: The saved orientation matrix of the corresponding assembly public Vector3 localposition: The saved local position of the corresponding assembly related to its parent assembly public Matrix localorientation: The saved local orientation matrix of the corresponding assembly related to its parent assembly public object userdata: The saved custom user data of the corresponding assembly public bool visible: The saved visibility state of the corresponding assembly public bool locked: The saved locked state of the corresponding assembly public float length: The saved length of the corresponding assembly public float width: The saved width of the corresponding assembly public float height:  The saved height of the corresponding assembly   Storing default property values:  As mentioned above Experior supports a mechanism to modify the default properties that a used to create new assemblies. These default values are saved by Experior so that they can be retrieved even after Experior is closed and started again.   To allow a user to change the default value the AssemblyInfo class contains a GetProperty and SetProperty methods: public string GetProperty(string tag): This method allows to retrieve the saved value of the property with as name the given tag. The value is returned as string.   public void SetProperty(string tag, object value): This method allows to save the value of the property with as name the given tag. Note that the value will be saved as a string.   Below is an example of its usage.   Suppose the assembly that we are creating has a property MinHeight. We want to store this for each assembly and therefore add a public field float MinHeight to the corresponding AssemblyInfo class. public class MyAssemblyInfo : AssemblyInfo { private static MyAssemblyInfo properties = new MyAssemblyInfo(); public float minHeight=0.2f; public static object Properties // used for showing default properties in property window when selecting icon of assembly in catalog window { get { properties.color = Experior.Core.Environment.Scene.DefaultColor; return properties; } } }   So by default the MinHeight value is 200 mm.   When we want to allow a user to change this default value and add this as a class property the following code can be added to the corresponding AssemblyInfo class: [XmlIgnore]// to make sure that the MinHeight property is not serialised for each assembly [CategoryAttribute("Size")] [Description("Minimum Height in mm")] [TypeConverter(typeof(FloatConverter))] [PropertyOrder(0)] public virtual float MinHeight { get { string value = GetProperty("MinHeight"); //retrieve the MinHeight property if (value == string.Empty) // property has not been stored yet minHeight = 0.3f; else minHeight = float.Parse(value); // assign the new value to the minHeight field return minHeight; } set { minHeight = value; SetProperty("MinHeight", value); // store the property } }   When a user selects the icon of the assembly in the catalog window then the selected method of the catalog is called. public override object Selected(string title, string subtitle) { if (title == "MyAssemblyClass") return MyAssemblyInfo.Properties; else ... }   As shown above this typically returns a static info object of the corresponding class which is used to display the default properties in the property window:   When changing the MinHeight value in the property window the setter of the MinHeight code shown above is executed and this causes to save the new value with the SetProperty method but also to modify the MinHeight field of the current info object. This info object is passed to the constructor of the matching Assembly class and hence the new value will be taken into account.  
View full article
Kasper.Vestrup Explorer
‎2026-01-14 06:20 AM

Labels:
  • Exp-6 Dev Assembly
  • Exp-6 Developer Guide
  • Experior 6
89 Views

Assembly

Before starting to build your first Assembly please read Assembly Configuration   Please have a look at the attached presentation that shows how easy it is to create an assembly.   You can download the attached zip file that includes a Visual Studio Project with the assembly created in the presentation
View full article
Kasper.Vestrup Explorer
‎2026-01-14 06:15 AM

Labels:
  • Exp-6 Dev Assembly
  • Exp-6 Developer Guide
  • Experior 6
196 Views

Assembly Configuration

  Overview   When you build an assembly the Assembly Configuration object can automatically handle: Rendering of parts, measures and other assemblies (sub-assemblies) Disposing of parts and sub-assemblies Picking User movement and rotation with the mouse Locking and unlocking Selecting and deselecting   If you want to manually handle some of these methods you must override the according method (or property) in the Assembly class:   public virtual void Move(Vector3 delta) public virtual float Yaw public virtual Vector3 Position public virtual void Lock() public virtual void UnLock() public virtual PickResult Pick(Vector3 rayStart, Vector3 rayDirection) public virtual void Render() public virtual void Select() public virtual void DeSelect() public virtual void Dispose()   This part of the wiki will explain how to: Add a part, a measure or another assembly to the assembly Position, rotate and move parts using local coordinates Use movement and angular movements of parts Use the coordinate systems   All examples in this document refer to the assemblies in the DeveloperSamples catalog. Naming the coordinates   When referring to an assembly’s local coordinates the coordinate system on the blue conveyor in Figure 1 is being referred to. These coordinates will always follow the assembly.   The Global coordinates always refer to the coordinate system of the 3D environment with axis names: X, Y, Z, also illustrated in Figure 1. This coordinate system is never moved.   The third coordinate system, placed in the blue cube in Figure 1, is the parts own coordinate system. This coordinate system is not as important as the two others and is mainly used when using a LocalRotationPoint.   Figure 1   Classes, methods and properties   The Assembly Configuration object is not accessed directly. But it is used indirectly when using the following methods and properties in the RigidPart, Assembly and Measure classes.   For the RigidPart class the involved methods and properties are: public Vector3 LocalPosition public float LocalYaw public float LocalPitch public float LocalRoll public Vector3 LocalRotationPoint public void LocalMovement(Vector3 localVelocity, float lengthToMove) public void LocalMovement(Vector3 localVelocity, float lengthToMove, float rampUpLength, float rampDownLength) public void LocalAngularMovement(Vector3 angularVelocity, Vector3 radiansToRotate) public event FinishedMoving LocalMovingFinished; public event FinishedRotating LocalRotationFinished; public CoordinateSystem LocalCoordinateSystem   For the Assembly class the involved methods are: protected RigidPart Add(RigidPart part) protected RigidPart Add(RigidPart part, Vector3 localPosition) protected RigidPart Add(string name, RigidPart part) protected RigidPart Add(string name, RigidPart part, Vector3 localPosition) protected Assembly Add(Assembly assembly, Vector3 localPosition) protected Assembly Add(Assembly assembly, Vector3 localPosition, bool visible) protected Core.Dialog.Measure Add(Core.Dialog.Measure Measure) protected CoordinateSystem Add(CoordinateSystem c) protected bool Remove(RigidPart part) protected bool Remove(Core.Dialog.Measure measure) protected bool Remove(Assembly assembly) public Vector3 LocalPosition public float LocalYaw public float LocalPitch public float LocalRoll   For the Measure class the involved methods are: public Vector3 LocalStartPosition public Vector3 LocalEndPosition   Note that all methods and properties begins with Add, Remove or Local. Add parts, measures and assemblies Adding parts   Adding a part to an assembly is done by the method Add(RigidPart part).   Example: (See Sample0.cs for full source code) Add(new Cube(Color.Wheat, 0.5f, 0.5f, 0.5f));   Figure 2 Without further code the cube can now be selected, deselected, locked, unlocked, picked, moved and rotated.   To remove the part from the assembly: protected bool Remove(RigidPart part) Note: this does not dispose the part.   Positioning of parts using LocalPosition In Sample1.cs it is shown how to add 100 cubes and position them using the LocalPosition property. Example: (See Sample1.cs for full source code) for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { Add(new Cube(Color.Wheat, 0.5f, 0.5f, 0.5f), new Vector3(0, i, j)); } } Figure 3   When a part is added to an assembly it is recommended that the part is positioned using the LocalPosition property.    The property called Position always refers to a part’s global position.   Rotating parts using LocalYaw, LocalPitch and LocalRoll   In Sample2.cs two cubes are added. The LocalPitch value of the blue cube has been set to 0.56f.   Default values for LocalYaw, LocalPitch and LocalRoll are 0 and the units are radians.   Figure 4 Note: If a part is not added to an assembly, the properties Yaw, Pitch and Roll will refer to the global orientation.   When a part is added to an assembly these values refer to the rotation in the parts own coordinate system. See also the section about LocalRotationPoint.   Adding another assembly and a measure   It can be useful to add another assembly and treat it as a part (or as a sub-assembly). This can be done with the method:  protected Assembly Add(Assembly assembly, Vector3 localPosition)   When the assembly has been added it can be positioned and rotated with the following methods: public Vector3 LocalPosition public float LocalYaw public float LocalPitch public float LocalRoll LocalPosition, LocalYaw, LocalPitch and LocalRoll all use the coordinate values of the assembly that the sub-assembly has been added to.   To add a measure use the following method: protected Core.Dialog.Measure Add(Core.Dialog.Measure Measure)   When constructing the measure a start position and an end position must be provided. Local coordinates must be used for these values as they are used when adding the measure to the assembly as the LocalStartPosition and LocalEndPosition.   If later on these values need changing use the following measure methods: public Vector3 LocalStartPosition public Vector3 LocalEndPosition Figure 5   The file Sample3.cs contains an example on how to add an existing conveyor and a measure as illustrated in Figure 5.   To remove the assembly or the measures use the following methods: protected bool Remove(Assembly assembly) protected bool Remove(Core.Dialog.Measure measure) Note: this does not dispose of the assembly.   Linear and angular movement (Physics mode) LocalMovement and LocalAngularMovement   To create a movement of a part it is always possible to override the void Step(float deltatime) method in the Assembly and use this time-step to move the parts.   The Assembly Configuration gives you two methods to create movements without overriding the Step method.   One for a straight movement: public void LocalMovement(Vector3 localVelocity, float lengthToMove)   One for an angular movement: public void LocalAngularMovement(Vector3 angularVelocity, Vector3 radiansToRotate)   These methods are available for the RigidPart class as well as the CoordinateSystem class (see later section) but not for the Assembly class.   The parameter Vector3 angularVelocity has the form:   (Local yaw Velocity, Local pitch velocity, Local roll velocity) It uses the units radians/second.   The parameter Vector3 radiansToRotate has the form: (Delta yaw, Delta pitch, Delta roll) It uses radians as units.   Note: if using a positive Local yaw Velocity a positive Delta yaw must be used as a stop condition, because the rotation is around the local Y-axis in positive direction. Likewise, if using a negative Local yaw Velocity a negative Delta yaw must be used.   To know when the movement has finished the following events can be subscribed to: public event FinishedMoving LocalMovingFinished; public event FinishedRotating LocalRotationFinished; Example: (See Sample4.cs for full source code)   The method: LocalMovement(new Vector3(1, 0, 0), 1); This will create a movement with velocity (1,0,0) in local coordinates which stops after 1 meter. Note that the length must always be positive, otherwise the movement is ignored.   The method: LocalAngularMovement(new Vector3(1, 0, 0), new Vector3((float)Math.PI, 0, 0)); This will create a rotation which rotates around the local Y-axis with velocity 1 radian/second which stops when it has rotated PI degrees.   The method: LocalAngularMovement(new Vector3(-1, 0, 0), new Vector3(-(float)Math.PI, 0, 0)); This will create a rotation which rotates around the local Y-axis with velocity -1 radian/second which stops when it has rotated -PI degrees.   How to use coordinate systems When more advanced movements are created it can be useful to have several coordinate systems. This is, for example, the case when constructing a robot as the one in the Robot class in the DeveloperSamples catalog. When using more than one coordinate system a hierarchy of coordinate systems can be used, meaning that a coordinate system can have a subsystem and a parent system. The top most coordinate system (the one having no parent system) is used as the local coordinate system of the assembly.   Robot class In the Robot class in the DeveloperSamples catalog we use 5 coordinate systems as illustrated in Figure 8. The coordinate systems have been placed where the robot is able to move or rotate.   If the coordinate systems have been constructed and added to the assembly using the following method: protected CoordinateSystem AddCoordinateSystem(CoordinateSystem c)   Then parts can be added to the coordinate system. This can be done with either: coordinate0.AddPart(link0, new Vector3(-0.17f, 0.15f, 0));   or: link0.LocalCoordinateSystem = coordinate0; link0.LocalPosition = new Vector3(-0.17f, 0.15f, 0); When a part has been added to a coordinate system the LocalPosition property of the part will refer to that coordinate system. Figure 8   The coordinate system axes have the same colors as the coordinate system axes of the 3D environment of Experior, the X-axis is green, Y-axis is red and Z-axis is blue.   Constructing the Coordinate System The coordinate system class has the following constructor: public CoordinateSystem(CoordinateSystem SubSystem, Vector3 SubSystemPosition)   Here it can be specified which coordinate system should be the subsystem and where it should be placed SubSystemPosition. The subsystem will automatically be assigned to the coordinate system which will function as its parent system.   If the coordinate system should not have any subsystem use the constructor:   CoordinateSystem(null, Vector3.Empty); Linear and Angular Movement of Coordinate Systems   The CoordinateSystem class has some of the same methods and events as the IRigidPart class: public void LocalMovement(Vector3 localVelocity, float lengthToMove) public void LocalMovement(Vector3 localVelocity, float lengthToMove, float RampUpLength, float RampDownLength) public void LocalAngularMovement(Vector3 angularVelocity, Vector3 radiansToRotate) public event FinishedMoving LocalMovingFinished public event FinishedRotating LocalRotationFinished When these methods are used the coordinate system will rotate or move any parts that are added to the coordinate system.   Rotating a coordinate system relative its parent system   To rotate a coordinate system use the following method:   public void RotateRelativeParent(float deltaYaw, float deltaPitch, float deltaRoll) This will also rotate its subsystem if there is any.
View full article
Kasper.Vestrup Explorer
‎2026-01-14 06:10 AM

Labels:
  • Exp-6 Dev Assembly
  • Exp-6 Developer Guide
  • Experior 6
197 Views

Build a Controller

Step 1:   Make a C# project where the main class extends the Experior.Core.Controller or Experior.Core.Logic class. Experior.Core.Controller is for making controllers that interacts with a model running in the discrete event engine and Experior.Core.Logic is for controllers interacting with a model running in the physics engine.     Step 2:   To start Experior directly within Visual Studio set Start external program. Then you can insert break points in the controller code and debug the source code.     Step 3:   Make sure the output path is the same as Experior     Step 4:     Now build the model you want to control and link the compiled controller with the model.   You can add multiple controllers disable/enable a linked controller or unload an existing controller.  
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:59 AM

on ‎2026-01-15 05:59 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
267 Views

Load class

Loads in Experior are used to model pallets, boxes, bags, that are transported by material handling equipment like conveyors and lifts.   The main namespace for loads is Experior.Core.Loads and the Experior.Core.Loads.Load contains some factory methods for creating loads with different load types.   The Experior.Core.Loads.Load class derives from the RigidPart class and hence inherits a lot of its interaction with the PhysX engine from it.   Note that there are some important differences related to loads depending on whether Experior is running in physics mode or discrete mode. In physics mode a load is considered a dynamic object (unless you disable the load or make it Kinematic). This implies that the position of the load and its orientation is determined by the underlying nVidia Physx engine who calculates this by applying all the forces/torques on it. In discrete mode no physics engine is used. There it is important to know that, unless a load is made undeletable, it will always be on a Route (Experior.Core.Routes.Route) or an Action Point (Experior.Core.Routes.ActionPoint). So, by default, if a load is no longer on a Route/Action Point then it will be deleted by the underlying engine.   Below we will illustrate a set of the main methods/properties of the Experior.Core.Loads.Load class.   Some methods/properties related to the interaction of the loads with the physics engine Sleep(): This is used in the physics mode to make the load unresponsive to the physics engine. The load will no longer respond to the forces applied to it. Note that you can WakeUp/Enable a load by clicking on it in the working area.   WakeUp(): This is used in the physics mode to “wakeup” a sleeping load and make it respond again to the physics engine. The load will again respond to the forces applied to it.   Vector3 CenterOfMassOffsetLocalPosition: When the physics engine applies a force to a load to make it moving it applies this force into the center of mass of a load. By default a loads density is equally distributed over the whole volume of the load and hence the center of mass of the load is also the geometrical center of the load. By providing the CenterOfMassOffsetLocalPosition you make it possible to move this center of mass and e.g. put the center of mass lower than the geometrical center to decrease the collapsing of a load.   NOTE: The value provided to this property is an absolute value. The CenterOfMassOffsetLocalPosition property provided in the property window of a feed is a relative one where 1 represents 100%.   Example and illustration. In the example below the CenterOfMassOffsetLocalPosition of the loads with dimensions (0.5f,0,5f,0.5f) is set to (0, -0.2f, 0) when the load enters a sensor: So the force will be applied 200mm below the geometrical center of the load. myLoad.CenterOfMassOffsetLocalPosition = new Vector3(0, -0.2f, 0);   You can clearly see that the force on the load is applied lower than before it entered the sensor.   bool Gravity: This indicates whether a load is subject to gravitational force. If you set it to false a load will not fall, but will remain subject to other forces like the force applied to it on a conveyor.   float Density: This represents the (uniform) density of the load. It is used to calculate the weight of the load ( Volume calculates as length times width times height multiplied with the Density).   bool Embedded: This indicates whether the load is embedded in another entity and hence whether it should be saved when saving the model. When the Embedded property is true the load is not saved. (In discrete mode loads are never saved).   bool Enabled: Setting Enabled to false is similar to calling Sleep() and setting Enabled to True is similar to calling WakeUp().   bool Kinematic: By default a load is a dynamic object by the PhysX engine which implies that its position and orientation will be determined by the physics engine by calculating all forces/torques applied to the load. By setting the Kinematic property to true, the load is made a kinematic object instead of a dynamic one. This implies that the load will no longer respond to the forces applied to it and the position and orientation of the load is controlled by the user. (Using the Methods/properties related to positioning of a load described below).   bool Rigid: Returns true when the load is a rigid body for the PhysX engine.   bool Sleeping : Returns true when the load has been “put to sleep” and is longer taken into account by the physics engine.   DeSelect(): Deselects the load.   Select(): Selects the load. The properties of the load will be shown in the properties window and the load will get the Select color.   HighLight(Color color): Highlights the load by changing the highlight color to the given color. This is only a temporary change. When the load is UnHighLighted is restores its original color (given by the Color property.   UnHighLight(). UnHighlights the load, as a result it will get the color back as defined by the Color property.   Color Color: Returns/sets the normal color of the load (when not selected or highlighted).   bool Selectable: Indicates whether a load can be selected by the user. If you set this property to False the user will not be able to select the load.   bool Selected: Returns True when the load is currently selected, False otherwise.   bool Transparent: Returns True when the color of the load is transparent.   Methods/properties related to deleting of a load Dispose(): Called when deleting the load   bool Deletable : Property to indicate whether the load can be deleted. If this property is set to False, trying to delete the load will fail (e.g. when resetting the model, which normally deletes all loads in the model)   bool UserDeletable: Property to indicate whether the user can delete the load (e.g.by selecting it and pressing the Delete button)   event DisposeEvent OnDisposed : This event is called when the load is completely disposed (deleted)   event DisposeEvent OnDisposing : This event is called beginning to dispose a load   Methods/properties related to grouping/deleting of a load Remark: In discrete mode the grouping/ungrouping of loads happens instantaneously and immediately after calling the Group/UnGroup methods the grouped load or ungrouped loads are available. Due to the nature of the physics engine this is postponed until the physics engine has finished its cycle of applying forces to loads. So in physics mode the grouped load is available when the OnGrouped event is raised.   Group(List<Load> loads): The current load will be grouped with all loads from the given list. They loads are grouped at their current relative position and the current load becomes the master load.   Group(Load load): The current load will be grouped with the given load. Both loads keep their relative position.   Group(Load load, Vector3 localposition): The given load will be grouped with the current load. It will be positioned given the relative position.    Group(List<Load> loads, List<Vector3> positions, List<Matrix> orientations): The given load is grouped with all loads from the given list of loads. Each load of the list is positioned according to the given relative position as provided in the positions list and with the orientation as described in the given orientations list of orientation matrices.   Group(Load load, Vector3 localposition, Matrix localorientation): The given load will be grouped with the current load. It will be positioned given the relative position and with the relative orientation as defined in the given orientation matrix.   Group(Load load, Vector3 localposition, float localyaw, float localpitch, float localroll): The given load will be grouped with the current load. It will be positioned given the relative position and with the relative orientation as defined by the relative Euler angles : localyaw, localpitch, localroll.   UnGroup(List<Load> loads): ungroups all loads from the given list of loads from the current load (masterload).   UnGroup(int loads): this will ungroup the given amount of loads from the current load. If the given number is larger then the total number of grouped loads it will ungroup all of them. If the number is smaller it will ungoup the given amount. The sequence of ungrouping is as follows: the last one grouped will be ungrouped first.   UnGroup(Load load): UnGroups the given load from the current load (masterload).   UnGroup(): UnGroups all loads that form the current load (masterload).   Collection Grouped: Returns the collection of loads that are grouped with the current load (the masterload). The Grouped collection allows to request the Length, Width, Height of the combined grouped load and you can iterate over all slave loads using the Items property of the Grouped property of the master load.   bool IsGrouped: Returns true for a load that has been grouped with a masterload. The materload itself will return false for this IsGrouped property.   event GroupedEvent OnGrouped: Is called when the load has been grouped. (See remark above concerning physics/discrete mode). The delegate has as argument the sender (masterload) and the result.   Below you find an example and screenshots of grouping: int createdloads=1;int totalnumber=10; Core.Resources.Mesh mesh=Common.Meshes.Get("Tote_Red"); Load ld1 = Load.Create(mesh, Length, Height, Width); ld1.Switch(linkedactionpoint); while (createdloads < totalnumber) { // create 4 more loads of given mesh and group them Load ld = Load.Create(mesh, Length, Height, Width); ld1.Group(ld,new Vector3(0,(Height-0.01f)*createdloads,0));//have totes slightly overlap // ld1.Group(ld,new Vector3(0,(Height+0.05f)*createdloads,0)) // create empty space between loads createdloads++; }   Above example results in the following stacks of totes being created depending whether we had loads overlapping or with empty space between them: Example of iterating over the grouped loads:   Experior.Core.Environment.Log.Write("Total dimensions are " + ld1.Grouped.Length + "; " + ld1.Grouped.Height + "; " + ld1.Grouped.Width); foreach (Load grl in ld1.Grouped.Items) { Experior.Core.Environment.Log.Write(" " + grl.Length + "; " + grl.Height + "; " + grl.Width); }   Methods/properties related to movement of a load in discrete mode MoveTo(string destination): This method will initiate a load traveling on its current route to the actionpoint with the name equal to the given destination. If the destination actionpoint is not reachable through routes connected to the current route then a message will be logged :  ‘Can’t continue: “destination” is unreachable (“current actionpoint name“)’   MoveTo(string source, string destination): This method will initiate a load traveling on its current route from the source actionpoint to the actionpoint with the name equal to the given destination.   Release(): This method will release a load that is stopped.   Release(float delay): This method will release a load that is stopped after the given delay has passed. The delay is given in seconds.   Stop(): This method will stop a load on its Route/ActionPoint. The load will wait there until it is released.   Switch(Route to): This method  will instantaneously take the load and put it at the start of the given route.   Switch(Route to, float distance): This method  will instantaneously take the load and put it at the given distance from the start of the given route.   Switch(Route to, bool keepGlobalOrientation): This method  will instantaneously take the load and put it at the start of the given route. When the keepGlobalOrientation argument is true than the load will keep its current global orientation (Yaw, Pitch, Roll), otherwise it will use the properties of the route.   Switch(Route to, float distance, bool keepGlobalOrientation): This method  will instantaneously take the load and put it at the given distance from the start of the given route. When the keepGlobalOrientation argument is true than the load will keep its current global orientation (Yaw, Pitch, Roll), otherwise it will use the properties of the route.   Switch(string actionpoint, bool keepGlobalOrientation): This method will put the load on the actionpoint with the given name.When the keepGlobalOrientation argument is true than the load will keep its current global orientation (Yaw, Pitch, Roll), otherwise it will use the properties of the actionpoint. If no actionpoint can be found with the given name the call is ignored.   Switch(ActionPoint ap, bool keepGlobalOrientation): this method will put the load on the given actionpoint. When the keepGlobalOrientation argument is true than the load will keep its current global orientation (Yaw, Pitch, Roll), otherwise it will use the properties of the actionpoint. In the illustrations below you can see the difference between switching a load (to AP6) with the keepGlobalOrientation argument true or false:       Switch(string actionpoint): This method will put the load on the actionpoint with the given name. If no actionpoint can be found with the given name the call is ignored.   Switch(ActionPoint ap, ActionPoint.Edges edge): This method will put the load on the actionpoint. The edge argument has 3 possible values ActionPoint.Edges.Leading, ActionPoint.Edges.Trailing and the defaultActionPoint.Edges.Center. If its value is Edges.Trailing then the load will be put on the actionpoint in such a way that the position of the actionpoint matches the trailing back of the load. If its value is Edges.Leading then the load will be put on the actionpoint in such a way that the position of the actionpoint matches the leading front of the load.   Note: This assumes that there is sufficient room on the route to do the switch, meaning that the center of the load should be on the route. Suppose you have an ActionPoint at the start of a route. In that case it is only possible to do the switch with edge ActionPoint.Edges.Center or ActionPoint.Edges.Trailing because using ActionPoint.Edges.Leading implies that the center of the load would not fit on the route.   Switch(ActionPoint ap): This method will put the load on the given actionpoint.   ActionPoint ActionPoint: This getter property returns the current actionpoint of the load or null if the load is on a route and not at an actionpoint.   string GoTo: This property allows to get of the name of the current final destination actionpoint. By setting this property to the name of an actionpoint the load will set it as its final destination and travel towards it.   Vector3 RouteOffset: Normally a load travels on the route with the center of the load moving over the route. By using the RouteOffset property you can chnage this and have the center of the load moving at the given relative position from the route.   Below you see an illustration of loads moving where load.RouteOffset = new Vector3(1.2f, 0.3f, 0f);   bool Stopped: This getter property returns True when the load is currently stopped, False otherwise.   float WaitingTime: Setting this property makes a load wait at its current position. The waiting time is provided in seconds. When this waiting time is elapsed the load will trigger the FinishedWaiting event and continue.   FinishedWaiting FinishedWaitingEvent: This event is triggered by a load when its WaitingTime has elapsed.   Methods/properties related to attaching a load to a RigidPart public bool IsAttached: This getter property returns true when the load is currently attached to a RigidPart and hence if following the movement of this part.   event AttachedEvent OnUnAttached: This event is triggered when the load gets unattached of the RigidPart it was attached to.   event AttachedEvent OnAttached: This event is triggered when the load gets attached to a RigidPart was attached to.   Attaching loads is done using one of the Attach methods of the Experior.Core.Parts.RigidPart class.   Custom Data object UserData: The developer can add custom data to a load by using this UserData property. E.g you can create you own TransportOrder class containing data from the WMS like PurchaseOrder,Customer, Source, Destination, DueDate,… and attach it to the load: myload.UserData = aTransportOrder;   You can inspect the data again by casting the UserData back to the class it was given: string customer = (myload.UserData as TransportOrder).Customer;   Methods/properties related to prositioning of a load float Yaw: This getter/setter property reflects the rotation angle (in radians) of the load around the Y-axis   float Pitch: This getter/setter property reflects the rotation angle (in radians) of the load around the X-axis   float Roll: This getter/setter property reflects the rotation angle (in radians) of the load around the Z-axis   Vector3 Position: This property returns the global 3 dimensional coordinates of the center of the load. In the physics mode the position is determined by the physics engine or its movement on tracks and in discrete mode by its movement on the routes. When the load is made Kinematic or Disabled then it makes sense to directly set the position of the load.   Matrix Orientation: This property returns or sets the orientation of the load as defined in the Microsoft.DirectX.Matrix structure.   Translate(Vector3 distance, float time): This will translate a stopped load over the given vector during the given time. After the given time the load remains at its position after the translation until it is released. In case the load continues moving on a route it will jump back to its position on the route. So note that the load should not be moving on a route when calling this translate method otherwise you get some strange wave movement during the given time where the load is trying to translate over the given vector but is pulled back by it having to move on the route.   Translate(Action action, Vector3 distance, float time): This method is similar to the Translate method above except that at the end of the translation of the load over the given vector distance the given action will be executed. The given System.Action is a delegate that you can use to pass a method as a parameter without explicitly declaring a custom delegate. The encapsulated method must correspond to the method signature that is defined by this delegate.This means that the encapsulated method must have no parameters and no return value.   In the example below a load is translated over 3 meter in the Y-direction in 2 seconds and at the end of the movement the load is switched to actionpoint  “AP10” and then released : currentload.Translate(()=>SwitchToAP10(), new Vector3(0, 3.0f, 0), 2f);   where: public void SwitchToAP10() { if(currentload != null) { currentload.Switch("AP10"); currentload.Release(); } }
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:55 AM

on ‎2026-01-15 05:55 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
189 Views

Extending the GUI with a Plugin

A plugin can be used to add custom functionality to Experior and you can also use it to extend the GUI by adding menu items and/or adding your own forms. A plugin is a dll that will be automatically loaded by Experior (provided the dll can be found in the installation directory of Experior). You can create your own plugin by creating a separate Visual Studio project where your plugin class derives from Experior.Core.Plugin.    Toolbar.Button To extend the GUI you can create a Button (Core.Environment.UI.Toolbar.Button) and add it to a toolbar.   In the example below a button is created and added to Model Toolbar in a Tab with name TestPlugin : Core.Environment.UI.Toolbar.Button IOButton = new Core.Environment.UI.Toolbar.Button("SensorOutputs", btnIOToggle_Click); IOButton.Tooltip = "Show outputs from sensors"; // add the button to the Model toolbar in a Tab with name "TestPlugin" Core.Environment.UI.Toolbar.Add(IOButton, "TestPlugin");   This will look as follows:   When the example SensorOutputs button is clicked it will trigger the associated btnIOToggle_Click method.   In this example this will update a custom form.   Experior.Core.Forms.Form Experior allows to add your own forms. You create your form similar to a normal System.Windows.Forms.Form.   However if you want to have a form that behaves similar to other Experior forms and is dockable, the created form should derive from Experior.Core.Forms.Form instead.   Also the FormType property has to be overridden. public override Core.Forms.Docking.DockContent.FormTypes FormType { get { return Core.Forms.Docking.DockContent.FormTypes.Permanent; } }   In attached example is a custom form that contains a datagrid will info of all Outputs for the sensors in your model. The form looks as follows when it is docked:   You can download the complete plugin attached to this article
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:47 AM

on ‎2026-01-15 05:47 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
142 Views

Rigid Sensor Part

RigidSensorPart The class Experior.Core.Parts.Sensors.RigidSensorPart derives from the abstract class Experior.Core.Parts.RigidPart. RigidSensorPart objects are used in the PhysX environment to detect and manage collisions with RigidParts such as Loads and Assemblies. Since they rely on the collision detection of the PhysX engine they have no functionality in the discrete mode. RigidSensorParts are available in different shapes (Experior.Core.Parts.Sensors.Cube, Experior.Core.Parts.Sensors.Sphere, Experior.Core.Parts.Sensors.Cylinder) and with different functionality (Experior.Core.Parts.Sensors.LoadMagnet and Experior.Core.Parts.Sensors.EaterCube). All sensor assemblies of the Sensor catalog contain at least one RigidSensorPart.   An example of the creation and usage of a RigidSensorPart can be found here.   Events event EnterEvent Enter; This event is called when the sensorpart collides with a part it should detect according to its Collision property. The delegate it will call has two arguments, the activated sensorpart and the triggering object (can be cast to Load or Assembly).   event LeaveEvent Leave; This event is called when the sensorpart collides with a part it should detect according to its Collision property. The delegate it will call has two arguments, the activated sensorpart and the triggering object (can be cast to Load or Assembly).   Properties virtual bool Active This property returns true when the sensorpart collides with a part it should detect according to its Collision property, false otherwise.   Experior.Core.Environment.Collisions Collision The Collision property has 3 possible values : Core.Environment.Collisions.Both, Core.Environment.Collisions.Loads and Core.Environment.Collisions.Equipment. Core.Environment.Collisions.Both: in this case the sensorpart will detect loads as well as assemblies Core.Environment.Collisions.Loads: in this case the sensorpart only detects loads Core.Environment.Collisions.Equipment: in this case the sensorpart detects rigidparts of an assembly   List<Load> Loads This property returns the list of all loads that are currently colliding with the sensorpart.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:43 AM

on ‎2026-01-15 05:43 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
160 Views

Interaction with Excel files

Experior provides some methods to read data from an Excel file and to write data into an Excel file.   To do this you can use the Experior.Core.Data.Excel class with the following methods. public static List<List<string>> Read(string filename, string sheetname); public void Write(string filename, string sheetname, List<List<string>> records);   There is also the possibility to verify whether a worksheet exists in a an Excel file with a given name. public static bool Exists(string filename, string sheetname);   By using the classes Excel, ExcelWorkbook, ExcelWorkSheet, from the namespace Experior.Core.Data.OfficeOpenXml you can obtain even more direct control.   For example. string filePath = Experior.Core.Directories.Model + "\\Simulation_results_" + DateTime.Now.Day.ToString() + "_" + DateTime.Now.Month.ToString() + "_" + DateTime.Now.Year.ToString() + "_" + DateTime.Now.Hour.ToString() + "_" + DateTime.Now.Minute.ToString() + "_.xlsx"; // create a new Excel workbook with the given filename and add a Worksheeet named "Simulation results" Experior.Core.Data.OfficeOpenXml.Excel e = new Core.Data.OfficeOpenXml.Excel(new System.IO.FileInfo(filePath)); Experior.Core.Data.OfficeOpenXml.ExcelWorksheet worksheet = e.Workbook.Worksheets.Add("Simulation results"); // write the data into the cells of the worksheet worksheet.Cells[1, 1].Value = "Total occupation time conveyor system"; worksheet.Cells[1, 2].Value = swTotalOnConv.elapsed.ToString(); worksheet.Cells[2, 1].Value = "Waiting time drivers"; for (int i = 0; i < driverWaitingTime.Count; i++) { worksheet.Cells[3 + i, 1].Value = "Waiting time after pattern " + (i + 1).ToString(); worksheet.Cells[3 + i, 1].Value = driverWaitingTime[i].elapsed.ToString("c"); } // Add another worksheet named "Simulated Pattern" Experior.Core.Data.OfficeOpenXml.ExcelWorksheet worksheetPattern = e.Workbook.Worksheets.Add("Simulated pattern"); r = 1; c = 1; foreach (List<string> rows in SimulatedPatternSheet) { c = 1; foreach (string cell in rows) { worksheetPattern.Cells[r, c].Value = cell; c++; } r++; } //save the Excel workbook file e.Save();
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:42 AM

on ‎2026-01-15 05:42 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
158 Views

RigidPart

The Experior.Core.Parts.RigidPart class is the base abstract class to encapsulate rigid bodies that are used by the NVIDIA PhysX engine.   Some important classes that derive from RigidPart are RigidSensorPart (used by sensors) and RigidLoadPart (used inside loads).   Methods & Properties related to attaching loads. The methods below are used to attach loads to a rigidpart and keep them at a given relative position and orientation to the RigidPart. For the physics engine the loads are no longer dynamic actors that are moved according to forces and torques that are applied to them, but instead the loads become kinematic objects after attaching them to a RigidPart. You can use these method e.g. to attach a load to a gripper of a robot or keep it on a fast moving shuttle.   Note: There is an important distinction between the different Attach methods related to the collision of the load with a sensor. This distinction is necessary due to a limitation of the PhysX engine. The physics engine does not give a notification if you use the program to change the position of a load that is in contact with a sensor. Therefore, in attach methods that change the position/orientation of the load the load is taken out of the sensors it was colliding with by the programming (generating the Leave event of the sensor to allow reacting upon this change).   Attach(Load load): This method will attach the given load to the current RigidPart.This implies that if the RigidPart moves/rotates that the attached load will move with it, keeping its relative position/orientation as when it was attached. When the load was colliding with a sensor the Leave event of the sensor is not called (see Note above).   Attach(List<Load> loads): This method will attach all loads from the given list to the current RigidPart. When the loads were colliding with a sensor the Leave event of the sensor is not called (see Note above).   Attach(Load load, Vector3 localposition): This method will attach the given load to the current RigidPart at the given relative localposition from the origin of the rigidpart.   Attach(Load load, Vector3 localposition, float localyaw, float localpitch, float localroll): This method will attach the given load to the current RigidPart at the given relative localposition from the origin of the rigidpart and with a relative orientation determined by the given localyaw, localpitch,localroll Euler angles.   Attach(Load load, Vector3 localposition, Matrix localorientation): This method will attach the given load to the current RigidPart at the given relative localposition from the origin of the rigidpart and with a relative orientation determined by the given orientation matrix localorientation.   Attach(List<Load> loads, List<Vector3> positions, List<Matrix> orientations): This method will attach all loads from the given list to the current RigidPart. The given positions list and given orientations list contains the releative position and relative orientation for the load with the same index in the loads list.   UnAttach(Load load): Unattach the given load from the current RigidPart and make it a dynamic actor again for the physics engine.   UnAttach(): Unattach all loads that are attached to the current RigidPart and make them dynamic actors again for the physics engine.   In the example below a load is attached to the sensor upon the Enter event. private void Entering(Core.Parts.Sensors.RigidSensorPart sensor, object trigger) { //already attached if (((Core.Loads.Load)trigger).IsAttached) return; //avoid that the load gets selected ((Core.Loads.Load)trigger).Selectable = false; //attach load at current relative position/orientation to the sensor part sensor.Attach((Core.Loads.Load)trigger); }   Methods & Properties related to PhysX interaction.   Actor Actor: This property sets/returns the instance of the Experior.PhysX.Actor class representing this RigidPart which is used inside the NVIDIA PhysX engine (NxActor class). Actors are the main objects in a physx simulation.   Experior.Core.Parts.Friction Friction: This property sets/returns the friction definition for this RigidPart as used by the physics engine. The Experior.Core.Parts.Friction class defines the static and the dynamic friction and has some predefined Friction configurations for the user’s convenience. These are Friction.Coefficients.Slippy, Friction.Coefficients.Sticky, Friction.Coefficients.Smooth & Friction.Coefficients.None. When you require more control over the static and dynamic friction values used for your RigidPart then you can provide custom values for the Static and Dynamic property of the Friction property when using Friction.Coefficients.Custom. var cube = new Experior.Core.Parts.Cube(System.Drawing.Color.DarkGray, info.length, info.height, info.width); cube.Friction.Coefficient = Friction.Coefficients.Slippy;   bool Kinematic: By setting the Kinematic property to true, the rigidpart is made a kinematic object instead of a dynamic one. This implies that the part will no longer respond to the forces applied to it and the position and orientation of the part is controlled by the user. When kinematic is false then the part is considered a dynamic object for the physics engine.   Methods & Properties related to relative positioning. bool Configured: This getter property returns true when the RigidPart is added to a parent object (e.g. assembly) and positioned using the LocalPosition, LocalYaw, properties.    float LocalYaw: This getter/setter property reflects the rotation angle (in radians) of the RigidPart around the Y-axis of its parent object.    float LocalPitch: This getter/setter property reflects the rotation angle (in radians) of the RigidPart around the X-axis of its parent object.    float LocalRoll: This getter/setter property reflects the rotation angle (in radians) of the RigidPart around the Z-axis of its parent object.    Matrix LocalOrientation: This property returns or sets the relative orientation of the RigidPart with respect to its parent object as defined in the Microsoft.DirectX.Matrix structure   Vector3 LocalPosition: This property returns or sets the relative 3 dimensional coordinates of the center of the RigidPart with respect to its parent object   Methods & Properties related to editing. bool Dragable: Tthis getter/setter property controls whether you can drag the RigidPart, e.g. The start and end fixpoints in the conveyor assemblies are Dragable by default to allow the user to change the length of the conveyor by dragging the fixpoints to another position. bool Locked: This getter/setter property allows to lock/unlock the RigidPart. When the part is locked it will change color to Colors.LOCKEDCOLOR (Yellow by default). bool Selectable: This getter/setter property allows to control whether the RigidPart can be selected (e.g. by clicking on it). IEntity Parent: This getter property returns the parent entity  form the RigidPart. If a RigidPart is added to an Assembly using then the Parent will return this Assembly.   Property related to rendering RenderingMode RenderOption: This getter/setter property is used to get/set how the RigidPart should be rendered. The following values are possible:   Primitive: The part will only be rendered when the Experior.Core.Environment.Scene.PresentationLevel of Experior is Core.Environment.Scene.PresentationLevels.Primitives   PrimitiveAndNormal: The part will only be rendered when the Experior.Core.Environment.Scene.PresentationLevel is Scene.PresentationLevels.Primitives or Scene.PresentationLevels.Normal or Scene.PresentationLevels.Detailed or only its wireframe when Scene.PresentationLevels.Wireframe.   Normal: The part will only be rendered when the Experior.Core.Environment.Scene.PresentationLevel is Scene.PresentationLevels.Normal, Scene.PresentationLevels.Detailed or only its wireframe when Scene.PresentationLevels.Wireframe   Transparent: The part will only be rendered transparently (you can see through it) when the Experior.Core.Environment.Scene.PresentationLevel is Scene.PresentationLevels.Normal, Scene.PresentationLevels.Detailed. Note: Transparency is only visible in Locked mode, in Edit mode the part is rendered nontransparent.   TransparentPrimitiveAndNormal: The part will only be rendered transparently (you can see through it) when the Experior.Core.Environment.Scene.PresentationLevel is Scene.PresentationLevels.Normal, Scene.PresentationLevels.Detailed and Scene.PresentationLevels.Primitives. Note: Transparency is only visible in Locked mode, in Edit mode the part is rendered nontransparent.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:41 AM

on ‎2026-01-15 05:41 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
85 Views

Open Office XML

The Experior.Core.Data library includes a tool for creating Office Open XML spreadsheets   The sample below shows how to create a work book with a sheet called “Data” FileInfo file = new FileInfo(@"c:\Test.xlsx"); if (file.Exists) { file.Delete(); // ensures we create a new workbook file= new FileInfo(@"c:\Test.xlsx"); } using (Experior.Core.Data.OfficeOpenXml.Excel excel = new Experior.Core.Data.OfficeOpenXml.Excel(newFile)) { Experior.Core.Data.OfficeOpenXml.ExcelWorksheet sheet = excel.Workbook.Worksheets.Add("Date"); sheet.Cells[1, 1].Value = "Data"; sheet.Cells[3, 1].Value = "1"; sheet.Cells[3, 2].Value = "2"; sheet.Cells[4, 1].Value = "3"; sheet.Cells[4, 2].Value = "4"; excel.Save(); }   Result:     Use the sheet:Cells[x,y].Style to set background color, font, border etc.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:39 AM

on ‎2026-01-15 05:39 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
173 Views

Internal communication

Experior provides an infrastructure for sending messages between objects.   In the Developer samples catalog an example of how to do this can be found.   The basic steps are; Register an object as a listener. Core.Communication.Internal.AddListener(object listener, RecieveMessage method)   For example. Core.Communication.Internal.AddListener(this, ReceiveMethod);   Where a method like this should be provided. void ReceiveMethod(object sender, object reciever, object message, bool broadcast)   2. Send a message to a known receiver. Core.Communication.Internal.SendMessage(object sender, object reciever, object message)   Broadcast a message to all listeners. Core.Communication.Internal.BroadcastMessage(object sender, object message)   Broadcast a message to all listeners of a specific type. Core.Communication.Internal.BroadcastMessage(object sender, Type recieverType, object message)   Broadcast a message to all listeners of a specific type (FullName). Core.Communication.Internal.BroadcastMessage(object sender, string recieverTypeFullName, object message)   3. When an object should stop listen or is disposed it should be removed. Core.Communication.Internal.RemoveListener(object listener)   Note: The specified delegate of the reciever is executed synchronously.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:37 AM

on ‎2026-01-15 05:37 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
123 Views

Dynamic Properties

You can add properties to assemblies at runtime.   To enable the feature the class has to use the attribute Experior.Core.Properties.DynamicObjectConverter.   Example: Adding properties two properties Properties.Add(newDynamicProperty { Name = "Value1", Type = typeof(int), Description = "custom property (integer)",Category = "Testing", Value = 1 }); Properties.Add(newDynamicProperty { Name = "Value2", Type = typeof(float), Description = "custom property (float)", Category = "Testing", Value = 10.0f });   By overriding the DynamicPropertyChanged method the object can handle the changes made to the properties added above public override void DynamicPropertyChanged(DynamicProperty property) { Log.Write(property.Name + " is changed to " + property.Value); }   See the sample DynamicProperties class in the Demo catalog for more information
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:36 AM

on ‎2026-01-15 05:36 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
103 Views

Reports – create custom class

This example shows how to create a custom statistics class that shows up in the generated report.   To add public values to the Statistics form window in Experior set the Observe value to true.   public class TestStatistics : Core.Reports.Statistics.Statistic { public override string Title { get { return "Test Statistics"; } } private double example; [DisplayName("Example field")] public double Example { get { return example; } set { if (value != example) { example = value; NotifyPropertyChanged("Example field"); } } } }
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:35 AM

on ‎2026-01-15 05:35 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
152 Views

Thread safe methods in Experior

To safely change properties and call methods in Experior be sure that you are running in the “Engine thread”. If the Property Experior.Core.Environment.InvokeRequried is false, then you are running in the “Engine thread”. Otherwise you need to invoke the method call.   Example code:   private void SomeMethod() { if (Experior.Core.Environment.InvokeRequired) { //Not running in Experior Engine thread Experior.Core.Environment.Invoke(SomeMethod); return; } //Put method code here. //Code is executed in Experior engine thread //and it is safe to call Experior methods. ... }
View full article
Kasper.Vestrup Explorer
‎2026-01-15 05:34 AM

on ‎2026-01-15 05:34 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
122 Views

[KNOWN ISSUE] Creating a custom catalog or plugin for Experior 6 in Visual Studio 2022

Please note that if you are creating a custom catalog or plugin for Experior 6 in Visual Studio 2022, you will get an error message when trying to compile the .dll. This is because Visual Studio 2022 will not be able to locate experior.build.dll. The reason it can’t locate the file, is because it is 32 bit and Visual Studio 2022 (or newer) is 64 bit.   You will be able to compile a catalog/plugin with previous versions of Visual Studio, for example 2019 is confirmed working with Experior 6.   Are you experiencing this issue and need help, feel free to contact us on email experior.support@se.com
View full article
Kasper.Vestrup Explorer
‎2026-01-15 12:34 AM

on ‎2026-01-15 12:34 AM

Labels:
  • Exp-6 Developer Guide
  • Experior 6
130 Views

Logging and Diagnostic messages

To provide diagnostic, debug or logging messages Experior provides several mechanisms.   Logging The Experior.Core.Environment.Log class provides the following static methods to write messages to the Log window. public static void Write(Exception exception); public static void Write(string message); public static void Write(Exception se, int id); public static void Write(string message, bool underline); public static void Write(string message, Color color); public static void Write(string message, LogFilter filter); public static void Write(string message, string hightlight); public static void Write(string message, bool underline, LogFilter filter); public static void Write(string message, Color color, LogFilter filter); public static void Write(string message, string hightlight, Color color); public static void Write(string message, string hightlight, LogFilter filter); public static void Write(string message, Color color, bool underline, LogFilter filter); public static void Write(string message, string hightlight, Color color, bool underline); public static void Write(string message, string hightlight, Color color, LogFilter filter); public static void Write(string message, string hightlight, Color color, bool underline, LogFilter filter);   The meaning of the arguments is straightforward; The message is the string that will be logged to the log window (and/or file depending on the properties set by the user in Experior). The highlight is part of the message that will be shown in bold. The color is the color in which the message will be shown. Underline, this boolean indicates whether or not the message will be underlined. The filter corresponds with the chosen filter by the user in the properties of the Log;   To illustrate, the following code Experior.Core.Environment.Log.Write("Created sensorpart assembly", "sensorpart", CadetBlue, true, Communication ); Experior.Core.Environment.Log.Write("Created sensorpart assembly", "sensorpart", Red, false, Action); will be shown as follows (when no filter is applied);     Debug log   Similar to the regular logging there is also the possibility to only log your message in case the debugging option is set in Experior.   In this case the Experior.Core.Environment.Log.Debug class is used with the public static void Write(string message) method.   Example: Experior.Core.Environment.Log.Debug.Write("This is an example debug message");   Diagnostic info   Experior also provides the possibility to print diagnostic messages to other areas than the logging window.   Therefore the class Experior.Core.Environment.Diagnostic contains the following static methods: public static void Message(string message); public static void Message(string message, Color color); public static void Message(string sender, string message); public static void Message(string message, Color color, Environment.DiagnosticAction action); public static void Message(string sender, string message, Color color); public static void Message(string message, Color color, Environment.DiagnosticAction action, Environment.Signs sign); public static void Message(string sender, string message, Color color, Environment.DiagnosticAction action);   By default the messages are shown in the status area underneath the working area.   The arguments are;   Message: The information that will be shown. Sender: This string appended with a : will be shown before the message. It is mainly used to provide the origin of the message e.g. the Assembly name. Environment.DiagnosticAction: This is an enum with the following possible values; ALARM in this case the message will also be shown in the alarm window of experior TEMPORARY in this case the message will only be shown for couple of seconds TEMPORARYANDLOG in this case the message will only be shown for couple of seconds but it will also be printed to the log window (where it will remain) STICKY in this case the message will remain visible in the status area until a new message is requested or until the message is removed or the Clear() method is called of the Diagnostic class REMOVE way to remove a STICKY diagnostic message NONE Environment.Signs: This is an enum to indicate what icon should be attached to the message in the status area.
View full article
Kasper.Vestrup Explorer
‎2026-01-15 12:31 AM

on ‎2026-01-15 12:31 AM

Labels:
  • Exp-6 Dev Environment
  • Exp-6 Developer Guide
  • Experior 6
112 Views
  • « Previous
    • 1
    • 2
  • Next »
To The Top!

Forums

  • APC UPS Data Center Backup Solutions
  • EcoStruxure IT
  • EcoStruxure Geo SCADA Expert
  • Metering & Power Quality
  • Schneider Electric Wiser

Knowledge Center

Events & webinars

Ideas

Blogs

Get Started

  • Ask the Community
  • Community Guidelines
  • Community User Guide
  • How-To & Best Practice
  • Experts Leaderboard
  • Contact Support

    Ask our Experts

    Have a question related to our products, solutions or services? Get quick support on community Forums

    Email Us

    For Community platform-related support, please email us

Subscribing is a smart move!
You can subscribe to this board after you log in or create your free account.
Forum-Icon

Create your free account or log in to subscribe to the board - and gain access to more than 10,000+ support articles along with insights from experts and peers.

Register today for FREE

Register Now

Already have an account? Login

Terms & Conditions Privacy Notice Change your Cookie Settings © 2026 Schneider Electric

Welcome!

Welcome to your new personalized space.

of

Explore