diff --git a/Common/PlayerDivConfig.cs b/Common/PlayerDivConfig.cs
new file mode 100644
index 0000000..8609785
--- /dev/null
+++ b/Common/PlayerDivConfig.cs
@@ -0,0 +1,141 @@
+using Hi.Common;
+using Hi.Common.XmlUtils;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace Sample.Common;
+
+///
+/// Per-user visibility flags for the Player page's divisions (charts and
+/// info panels), flattened into a single all-boolean config persisted through
+/// .
+///
+///
+/// Defaults are tuned for a first-time user: the two info panels
+/// ( and ) are ON
+/// so the Player page looks populated without having to open the
+/// Division Visibility dropdown; every chart toggle starts OFF.
+///
+public class PlayerDivConfig : IMakeXmlSource
+{
+ #region XML IO
+ ///
+ /// Registers this type's deserializer with the given
+ /// (or when is
+ /// null). Idempotent.
+ ///
+ public static void Reg(XFactory factory = null)
+ {
+ factory ??= XFactory.Default;
+ factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
+ => new PlayerDivConfig(xml));
+ }
+
+ ///
+ /// Name for XML IO.
+ ///
+ public static string XName => nameof(PlayerDivConfig);
+
+ ///
+ /// Default constructor.
+ ///
+ public PlayerDivConfig() { }
+
+ ///
+ /// Initializes a new instance of the class from XML data.
+ ///
+ /// XML element containing player-div data.
+ public PlayerDivConfig(XElement src)
+ {
+ if (src == null) return;
+
+ src.Element(nameof(EnableStripAvailabilityChart))?.Value?.SelfInvoke(
+ v => EnableStripAvailabilityChart = XmlConvert.ToBoolean(v));
+ src.Element(nameof(EnableStripRoughnessChart))?.Value?.SelfInvoke(
+ v => EnableStripRoughnessChart = XmlConvert.ToBoolean(v));
+ src.Element(nameof(ColorIndexTimeChart))?.Value?.SelfInvoke(
+ v => ColorIndexTimeChart = XmlConvert.ToBoolean(v));
+ src.Element(nameof(ForceCycleLineDiv))?.Value?.SelfInvoke(
+ v => ForceCycleLineDiv = XmlConvert.ToBoolean(v));
+ src.Element(nameof(SimSpindleMomentCycleLineDiv))?.Value?.SelfInvoke(
+ v => SimSpindleMomentCycleLineDiv = XmlConvert.ToBoolean(v));
+ src.Element(nameof(SensorSpindleMomentCycleLineDiv))?.Value?.SelfInvoke(
+ v => SensorSpindleMomentCycleLineDiv = XmlConvert.ToBoolean(v));
+ src.Element(nameof(DynamometerForceCycleLineDiv))?.Value?.SelfInvoke(
+ v => DynamometerForceCycleLineDiv = XmlConvert.ToBoolean(v));
+ src.Element(nameof(EnableStepDiv))?.Value?.SelfInvoke(
+ v => EnableStepDiv = XmlConvert.ToBoolean(v));
+ src.Element(nameof(MessageTable))?.Value?.SelfInvoke(
+ v => MessageTable = XmlConvert.ToBoolean(v));
+ src.Element(nameof(EnableDetailDiv0))?.Value?.SelfInvoke(
+ v => EnableDetailDiv0 = XmlConvert.ToBoolean(v));
+ }
+
+ ///
+ public XElement MakeXmlSource(string baseDirectory, string relFile, bool exhibitionOnly)
+ {
+ return new XElement(XName,
+ new XElement(nameof(EnableStripAvailabilityChart), EnableStripAvailabilityChart),
+ new XElement(nameof(EnableStripRoughnessChart), EnableStripRoughnessChart),
+ new XElement(nameof(ColorIndexTimeChart), ColorIndexTimeChart),
+ new XElement(nameof(ForceCycleLineDiv), ForceCycleLineDiv),
+ new XElement(nameof(SimSpindleMomentCycleLineDiv), SimSpindleMomentCycleLineDiv),
+ new XElement(nameof(SensorSpindleMomentCycleLineDiv), SensorSpindleMomentCycleLineDiv),
+ new XElement(nameof(DynamometerForceCycleLineDiv), DynamometerForceCycleLineDiv),
+ new XElement(nameof(EnableStepDiv), EnableStepDiv),
+ new XElement(nameof(MessageTable), MessageTable),
+ new XElement(nameof(EnableDetailDiv0), EnableDetailDiv0)
+ );
+ }
+ #endregion
+
+ ///
+ /// Availability strip chart (per-step timeline coloured by availability aspect).
+ ///
+ public bool EnableStripAvailabilityChart { get; set; } = false;
+
+ ///
+ /// Surface-roughness strip chart (per-step timeline coloured by surface-roughness aspect).
+ ///
+ public bool EnableStripRoughnessChart { get; set; } = false;
+
+ ///
+ /// Color-index strip chart (single-strip timeline coloured by arbitrary numeric/bool aspect).
+ ///
+ public bool ColorIndexTimeChart { get; set; } = false;
+
+ ///
+ /// Cycle-line chart of cutting force along the step's rotation cycle.
+ ///
+ public bool ForceCycleLineDiv { get; set; } = false;
+
+ ///
+ /// Cycle-line chart of simulation-derived spindle moment on spindle-rotation coordinate.
+ ///
+ public bool SimSpindleMomentCycleLineDiv { get; set; } = false;
+
+ ///
+ /// Cycle-line chart of sensor-measured spindle moment (overlay on sim moment).
+ ///
+ public bool SensorSpindleMomentCycleLineDiv { get; set; } = false;
+
+ ///
+ /// Cycle-line chart of sensor-measured force from an external dynamometer.
+ ///
+ public bool DynamometerForceCycleLineDiv { get; set; } = false;
+
+ ///
+ /// Full / condensed step information panel (Selected Step Info).
+ ///
+ public bool EnableStepDiv { get; set; } = true;
+
+ ///
+ /// Session messages panel.
+ ///
+ public bool MessageTable { get; set; } = true;
+
+ ///
+ /// Developer-only raw step-property dump panel.
+ ///
+ public bool EnableDetailDiv0 { get; set; } = false;
+}
diff --git a/Common/UserConfig.cs b/Common/UserConfig.cs
new file mode 100644
index 0000000..9539216
--- /dev/null
+++ b/Common/UserConfig.cs
@@ -0,0 +1,173 @@
+using Hi.Cbtr;
+using Hi.Common;
+using Hi.Common.XmlUtils;
+using Hi.MachiningSteps;
+using Hi.NcMech.Fixtures;
+using Hi.NcMech.Workpieces;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace Sample.Common;
+
+///
+/// User Configuration.
+///
+///
+/// Sample-owned copy of the per-GUI user configuration, kept so the
+/// step-present demo still compiles. Each
+/// GUI/APP keeps its own version; the core engine package (HiNc) no longer
+/// carries any GUI configuration type.
+///
+public class UserConfig : IMakeXmlSource
+{
+ ///
+ /// Registers this type's deserializer with the given
+ /// (or when is
+ /// null). Idempotent.
+ ///
+ public static void Reg(XFactory factory = null)
+ {
+ factory ??= XFactory.Default;
+ factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
+ => new UserConfig(xml, baseDirectory));
+ }
+ #region XML IO
+ ///
+ /// Name for XML IO.
+ ///
+ public static string XName => nameof(UserConfig);
+ ///
+ /// Initializes a new instance of the class from XML data.
+ ///
+ /// XML element containing configuration data
+ /// Base directory for resolving relative paths
+ public UserConfig(XElement src, string baseDirectory)
+ {
+ if (src == null) return;
+
+ src.Element(nameof(ShowPhysicsOptions))?.Value?.SelfInvoke(
+ v => ShowPhysicsOptions = XmlConvert.ToBoolean(v));
+ src.Element(nameof(LanguageCode))?.Value?.SelfInvoke(v => LanguageCode = v);
+ src.Element(nameof(EnableFullControl))?.Value?.SelfInvoke(
+ v => EnableFullControl = XmlConvert.ToBoolean(v));
+
+ src.Element(nameof(GraphicCacheLowerLimitMb))?.Value?.SelfInvoke(
+ v => GraphicCacheLowerLimitMb = XmlConvert.ToDouble(v));
+ src.Element(nameof(GraphicCacheUpperLimitMb))?.Value?.SelfInvoke(
+ v => GraphicCacheUpperLimitMb = XmlConvert.ToDouble(v));
+ src.Element(nameof(GraphicCacheMb))?.Value?.SelfInvoke(
+ v => GraphicCacheMb = XmlConvert.ToInt64(v));
+
+ src.Element(nameof(DisplayedStepPresentKeyList))?.SelfInvoke(ee =>
+ {
+ DisplayedStepPresentKeyList = ee.Elements("Item").Select(e => e.Value).ToList();
+ });
+
+ // Load FixtureSetupDisplayeeConfig
+ src.Element(nameof(FixtureSetupDisplayeeConfig))?.SelfInvoke(e =>
+ {
+ FixtureSetupDisplayeeConfig = new FixtureEditorDisplayeeConfig(e);
+ });
+
+ // Load EquipmentWorkpieceSetupDisplayeeConfig
+ src.Element(nameof(EquipmentWorkpieceSetupDisplayeeConfig))?.SelfInvoke(e =>
+ {
+ EquipmentWorkpieceSetupDisplayeeConfig = new WorkpieceEditorDisplayeeConfig(e);
+ });
+
+ // Load PlayerDivConfig
+ src.Element(nameof(PlayerDivConfig))?.SelfInvoke(e =>
+ {
+ PlayerDivConfig = new PlayerDivConfig(e);
+ });
+ }
+ ///
+ public XElement MakeXmlSource(string baseDirectory, string relFile, bool exhibitionOnly)
+ {
+ XElement dst = new XElement(XName,
+ new XElement(nameof(ShowPhysicsOptions), ShowPhysicsOptions),
+ new XElement(nameof(LanguageCode), LanguageCode),
+ new XElement(nameof(EnableFullControl), EnableFullControl),
+ new XElement(nameof(GraphicCacheLowerLimitMb), GraphicCacheLowerLimitMb),
+ new XElement(nameof(GraphicCacheUpperLimitMb), GraphicCacheUpperLimitMb),
+ new XElement(nameof(GraphicCacheMb), GraphicCacheMb),
+ new XElement(nameof(DisplayedStepPresentKeyList),
+ DisplayedStepPresentKeyList.Select(v => new XElement("Item", v))),
+ new XElement(nameof(FixtureSetupDisplayeeConfig),
+ FixtureSetupDisplayeeConfig?.MakeXmlSource(baseDirectory, relFile, exhibitionOnly)),
+ new XElement(nameof(EquipmentWorkpieceSetupDisplayeeConfig),
+ EquipmentWorkpieceSetupDisplayeeConfig?.MakeXmlSource(baseDirectory, relFile, exhibitionOnly)),
+ PlayerDivConfig?.MakeXmlSource(baseDirectory, relFile, exhibitionOnly)
+ );
+ return dst;
+ }
+ #endregion
+
+ ///
+ /// Gets or sets whether to show physics options in the UI.
+ ///
+ public bool ShowPhysicsOptions { get; set; } = false;
+ ///
+ /// Gets or sets the language code for the application UI.
+ ///
+ public string LanguageCode { get; set; } = "en";
+ ///
+ /// Enable Full control of the application.
+ /// Eanble System.Diagnostics.Process in GUI Script Command.
+ /// Not used yet.
+ ///
+ public bool EnableFullControl { get; set; }
+ ///
+ /// Gets or sets the lower limit of graphic cache in megabytes.
+ ///
+ public double GraphicCacheLowerLimitMb { get; set; } = 10;
+ ///
+ /// Gets or sets the upper limit of graphic cache in megabytes.
+ ///
+ public double GraphicCacheUpperLimitMb { get; set; } = 1200;
+ ///
+ /// Gets or sets the graphic cache size in megabytes.
+ ///
+ public long GraphicCacheMb
+ {
+ get => CubeTree.DispCacheMb;
+ set => CubeTree.DispCacheMb = value;
+ }
+
+ ///
+ /// Step infomation key list to show.
+ ///
+ public List DisplayedStepPresentKeyList { get; set; } = new List([
+ nameof(MachiningStep.StepIndex),
+ nameof(MachiningStep.FileNo), nameof(MachiningStep.LineNo),
+ nameof(MachiningStep.FilePath),nameof(MachiningStep.AccumulatedTime),
+ nameof(MachiningStep.LineText),nameof(MachiningStep.FlagsText),
+ nameof(MachiningStep.ToolId),
+ nameof(MachiningStep.SpindleSpeed_rpm),nameof(MachiningStep.Feedrate_mmdmin),
+ nameof(MachiningStep.Mrr_mm3ds),
+ nameof(MachiningStep.AvgAbsTorque_Nm),
+ ]);
+
+ ///
+ /// Configuration for FixtureSetupDisplayee
+ ///
+ public FixtureEditorDisplayeeConfig FixtureSetupDisplayeeConfig { get; set; } = new FixtureEditorDisplayeeConfig();
+
+ ///
+ /// Configuration for EquipmentWorkpieceSetupDisplayee
+ ///
+ public WorkpieceEditorDisplayeeConfig EquipmentWorkpieceSetupDisplayeeConfig { get; set; } = new WorkpieceEditorDisplayeeConfig();
+
+ ///
+ /// Per-user visibility flags for the Player page's divisions (charts and info panels).
+ /// Persisted alongside the rest of this .
+ ///
+ public PlayerDivConfig PlayerDivConfig { get; set; } = new PlayerDivConfig();
+
+ ///
+ /// Default constructor
+ ///
+ public UserConfig() { }
+}
diff --git a/Common/UserService.cs b/Common/UserService.cs
new file mode 100644
index 0000000..8e11fdd
--- /dev/null
+++ b/Common/UserService.cs
@@ -0,0 +1,160 @@
+using Hi.Common;
+using Hi.Common.XmlUtils;
+using Hi.Licenses;
+using Hi.MachiningSteps;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Sample.Common;
+
+///
+/// User Service.
+///
+///
+/// Sample-owned copy, kept so the step-present
+/// demo still compiles. Each GUI/APP keeps its own version; the core engine
+/// package (HiNc) no longer carries this GUI service type.
+///
+public class UserService : IDisposable
+{
+ ///
+ /// Gets or sets the application configuration.
+ ///
+ public UserConfig UserConfig { get; set; } = new UserConfig();
+ ///
+ /// Gets or sets the path to the application configuration file.
+ ///
+ public string UserConfigPath { get; set; }
+ ///
+ /// Gets whether physics features are enabled based on configuration and license.
+ ///
+ public bool EnablePhysics =>
+ UserConfig.ShowPhysicsOptions && IsPhysicsLicensed;
+ ///
+ /// Gets whether advanced physics features are licensed.
+ ///
+ public bool IsPhysicsLicensed => License.IsLoggedIn(AuthFeature.AdvancedPhysics);
+
+ //runtime properties are not in the category of canonical IO.
+ #region Runtime Property
+ private readonly ILogger _logger;
+ LooseRunner SaveConfigLooseRunner;
+ ///
+ /// Gets or sets the currently selected item in the application.
+ ///
+ public object SelectedItem { get; set; }
+ private bool disposedValue;
+ #endregion
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ public UserService(ILogger logger) { _logger = logger; SaveConfigLooseRunner = new LooseRunner(logger); }
+ ///
+ /// Initializes a new instance of the class with the specified configuration.
+ ///
+ /// The application configuration.
+ /// The logger instance.
+ public UserService(UserConfig appConfig, ILogger logger) : this(logger) { UserConfig = appConfig; }
+
+ ///
+ /// Saves the user configuration to the file specified by .
+ ///
+ public void SaveUserConfig()
+ {
+ try
+ {
+ if (UserConfigPath != null)
+ UserConfig.MakeXmlSourceToFile(UserConfigPath);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "{Message}", ex.Message);
+ }
+ }
+ ///
+ /// Schedules a loose save of the user configuration using a LooseRunner.
+ ///
+ public void LooseSaveUserConfig()
+ {
+ SaveConfigLooseRunner.TryRun(token => SaveUserConfig());
+ }
+
+ #region step present
+ ///
+ /// Gets or sets additional step presentation access configurations.
+ ///
+ public Dictionary AdditionalStepPresentAccess { get; set; } = new Dictionary();
+ ///
+ /// StepPresentAccessDictionary.
+ /// Read only.
+ ///
+ public Dictionary StepPresentAccessDictionary
+ {
+ get
+ {
+ var originalPresentAccessDictionary = typeof(MachiningStep).GetProperties()
+ .Where(p => p.GetCustomAttribute() != null)
+ .ToDictionary(prop => prop.Name, prop =>
+ new PresentAccess(prop.GetCustomAttribute(),
+ step => prop.GetValue(step)));
+ var presentAccessDictionary = new Dictionary(
+ AdditionalStepPresentAccess.Concat(originalPresentAccessDictionary));
+ return presentAccessDictionary;
+ }
+ }
+ ///
+ /// Candidate Step Present Key List for display.
+ /// Read only.
+ ///
+ public List CandidateStepPresentKeyList => StepPresentAccessDictionary.Keys.ToList();
+ ///
+ /// StepPresentAccessList for display.
+ /// Read only.
+ ///
+ public List> DisplayedStepPresentAccessList
+ {
+ get
+ {
+ var displayedStepPresentKeyList = UserConfig.DisplayedStepPresentKeyList;
+ List> dst
+ = new List>(
+ displayedStepPresentKeyList.Count);
+ var stepPresentAccessDictionary = StepPresentAccessDictionary;
+ foreach (var key in displayedStepPresentKeyList)
+ {
+ if (stepPresentAccessDictionary.TryGetValue(
+ key, out var access))
+ {
+ dst.Add(new KeyValuePair(key, access));
+ }
+ }
+ return dst;
+ }
+ }
+ #endregion
+
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ SaveConfigLooseRunner.Dispose();
+ }
+ disposedValue = true;
+ }
+ }
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}