Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Runtime/Scripts/Communication/Client/ClientVariableExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text.RegularExpressions;
using UnityEngine;

namespace OC.Communication
{
public static class ClientVariableExtension
{
private static readonly Regex InvalidChars = new ("[^A-Za-z0-9_]", RegexOptions.Compiled);

/// <summary>
/// Returns true if:
/// - name is not null/empty
/// - first char is a letter
/// - all chars are letters, digits or underscore
/// </summary>
public static bool IsVariableNameValid(string name)
{
if (string.IsNullOrEmpty(name)) return false;
if (!char.IsLetter(name[0])) return false;

for (var i = 1; i < name.Length; i++)
{
var character = name[i];
if (!(char.IsLetterOrDigit(character) || character == '_')) return false;
}

return true;
}

/// <summary>
/// Replace spaces and hyphens with underscore
/// Remove any other invalid characters
/// Ensure it starts with a letter by prefixing 'A' if needed
/// </summary>
public static string CorrectVariableName(string input)
{
if (string.IsNullOrEmpty(input)) return input;

var withUnderscores = Regex.Replace(input, @"[\s-]+", "_");
var cleaned = InvalidChars.Replace(withUnderscores, "");

if (char.IsLetter(cleaned[0])) return cleaned;

var name = "A" + cleaned;

cleaned = name;

return cleaned;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Xml.Linq;
using UnityEngine;
using AdsClient = TwinCAT.Ads.TcAdsClient;

namespace OC.Communication.TwinCAT
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public bool Verbose
Name = "Client",
Reconnect = true,
NetId = "Local",
Port = 351,
Port = 851,
ClearBuffer = true,
Verbose = false
};
Expand Down
33 changes: 32 additions & 1 deletion Runtime/Scripts/Communication/Link/Hierarchy.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#if UNITY_EDITOR
using UnityEditor;
#endif

using UnityEngine;

namespace OC.Communication
Expand All @@ -17,7 +21,34 @@ public class Hierarchy : MonoBehaviour

private string GetName()
{
return string.IsNullOrEmpty(_name) ? transform.name : _name;
if (string.IsNullOrEmpty(_name))
{
if (!ClientVariableExtension.IsVariableNameValid(transform.name))
{
var validName = ClientVariableExtension.CorrectVariableName(transform.name);
#if UNITY_EDITOR
Debug.LogWarning($"Hierarchy GameObject name {transform.name} is invalid! The name is modified to {validName}", this);
transform.name = validName;
EditorUtility.SetDirty(this);
#endif
}

return transform.name;
}
else
{
if (!ClientVariableExtension.IsVariableNameValid(_name))
{
var validCustomName = ClientVariableExtension.CorrectVariableName(_name);
#if UNITY_EDITOR
Debug.LogWarning($"Hierarchy name {_name} is invalid! The name is modified to {validCustomName}");
_name = validCustomName;
EditorUtility.SetDirty(this);
#endif
}

return _name;
}
}

public Transform GetParent()
Expand Down
4 changes: 2 additions & 2 deletions Runtime/Scripts/Communication/Link/Link.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public Link(Component component)
public void Initialize(Component component)
{
_component = component;
_name = this.GetName();
_path = this.GetPath();
_name = this.GetHierarchyName();
_path = this.GetHierarchyPath();
_client = this.GetClient();
_connectors = new List<Connector>();
}
Expand Down
65 changes: 62 additions & 3 deletions Runtime/Scripts/Communication/Link/LinkExtension.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace OC.Communication
{
public static class LinkExtension
{
/// <summary>
/// Retrieves the name of the GameObject associated with the given <see cref="Link"/>.
/// If the name is not a valid variable name according to <see cref="ClientVariableExtension"/>,
/// it will be corrected on the GameObject and a warning will be logged in the Unity Editor.
/// </summary>
/// <param name="link">The <see cref="Link"/> whose GameObject name is being retrieved and validated.</param>
/// <returns>The (original) name of the GameObject. Note that the GameObject’s name is modified in-editor if it was invalid.</returns>
public static string GetName(this Link link)
{
var name = link.Component.gameObject.name;

if (!ClientVariableExtension.IsVariableNameValid(name))
{
var oldName = name;
name = ClientVariableExtension.CorrectVariableName(name);
#if UNITY_EDITOR
Debug.LogWarning($"Link component name {oldName} is invalid! The name is modified to {name}", link.Component);
link.Component.gameObject.name = name;
EditorUtility.SetDirty(link.Component);
#endif
}

return name;
}

/// <summary>
/// Constructs the full hierarchical name for the GameObject associated with the given <see cref="Link"/>.
/// Starting from the GameObject’s own name, it traverses upward through its parent transforms.
/// For each parent that has a <see cref="Hierarchy"/> component marked as a sampler (<see cref="Hierarchy.IsNameSampler"/>),
/// its <see cref="Hierarchy.Name"/> is prepended (followed by an underscore) to the current name.
/// The process stops when there are no more sampler hierarchies in the chain.
/// </summary>
/// <param name="link">The <see cref="Link"/> whose GameObject full name is being assembled.</param>
/// <returns>
/// A string representing the combined sampler names and the original GameObject name,
/// separated by underscores (e.g. "RootSampler_SubSampler_ObjectName").
/// </returns>
public static string GetHierarchyName(this Link link)
{
var name = link.GetName();
var parent = link.Parent != null ? link.Parent.transform : link.Component.transform.parent;

while (parent != null)
Expand All @@ -31,10 +71,29 @@ public static string GetName(this Link link)

return name;
}

public static string GetPath(this Link link, bool original = false)

/// <summary>
/// Builds a dot-separated path representing the link’s position in the client/hierarchy structure.
/// Starts with the link’s own name (validated via <see cref="GetName"/>), then walks up through
/// parent transforms. If a <see cref="Client"/> is encountered, its <see cref="Client.RootName"/>
/// is prepended and the traversal ends. Otherwise, for each <see cref="Hierarchy"/> parent, its
/// <see cref="Hierarchy.Name"/> is prepended using “.” or “_” if <see cref="Hierarchy.IsNameSampler"/>
/// is true. If <paramref name="original"/> is false and the link has a <see cref="Link.Parent"/>,
/// the traversal uses that chain; otherwise it uses the GameObject’s raw transform hierarchy.
/// </summary>
/// <param name="link">The <see cref="Link"/> whose hierarchy path is being constructed.</param>
/// <param name="original">
/// If false and <paramref name="link"/> has a non-null <see cref="Link.Parent"/>, use the parent link’s
/// transform chain; otherwise use the GameObject’s direct transform parents.
/// </param>
/// <returns>
/// A string combining client root and hierarchy names, separated by “.” (or “_” for samplers),
/// e.g. “RootClient.ParentName.ChildName” or “Sampler1_Sampler2_ObjectName”.
/// </returns>
public static string GetHierarchyPath(this Link link, bool original = false)
{
var path = link.Component.gameObject.name;
var path = link.GetName();

Transform parent;

if (!original && link.Parent != null)
Expand Down
2 changes: 1 addition & 1 deletion Runtime/Scripts/System/ProjectTreeFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static XElement CreateDevice(Link link)

if (!link.IsPathOriginal())
{
var path = link.GetPath(true);
var path = link.GetHierarchyPath(true);
device.Add(new XElement("OriginalPath", path));
}

Expand Down
6 changes: 3 additions & 3 deletions Samples/Demo/0.1 Devices.unity
Original file line number Diff line number Diff line change
Expand Up @@ -1092,8 +1092,8 @@ MonoBehaviour:
_type: FB_Cylinder
_parent: {fileID: 0}
_attributes: []
_name: Cylinder_1
_path: MAIN.Devices.Cylinders.Cylinder_1
_name: Cylinder_11
_path: MAIN.Devices.Cylinders.Cylinder_11
_connector:
Control: 0
Status: 0
Expand Down Expand Up @@ -10505,7 +10505,7 @@ MonoBehaviour:
_name: Client
_reconnect: 1
_netId: Local
_port: 351
_port: 851
_clearBuffer: 1
_verbose: 0
--- !u!1 &2008748801
Expand Down
3 changes: 3 additions & 0 deletions Tests/Editor/Client.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Tests/Editor/Client/TestClientVariableExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using NUnit.Framework;
using OC.Communication;

namespace OC.Editor.Tests.Client
{
public class TestClientVariableExtension
{
[Test]
[TestCase("Machine&3", ExpectedResult = false)]
[TestCase("_FG01", ExpectedResult = false)]
[TestCase("0001", ExpectedResult = false)]
[TestCase("FG 01", ExpectedResult = false)]
[TestCase("FG-01", ExpectedResult = false)]
[TestCase("FG01", ExpectedResult = true)]
[TestCase("FG_01", ExpectedResult = true)]
public bool IsVariableNameValid(string name)
{
return ClientVariableExtension.IsVariableNameValid(name);
}

[Test]
[TestCase("Machine&3", ExpectedResult = "Machine3")]
[TestCase("_FG01", ExpectedResult = "A_FG01")]
[TestCase("0001", ExpectedResult = "A0001")]
[TestCase("FG 01", ExpectedResult = "FG_01")]
[TestCase("FG-01", ExpectedResult = "FG_01")]
[TestCase("FG01", ExpectedResult = "FG01")]
[TestCase("FG_01", ExpectedResult = "FG_01")]
public string CorrectVariableName(string input)
{
return ClientVariableExtension.CorrectVariableName(input);
}
}
}
3 changes: 3 additions & 0 deletions Tests/Editor/Client/TestClientVariableExtension.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.