Quickstart
Use this page to get from zero to a working TonCut Desktop plugin. The starter below keeps the full ITcPlugin surface visible from the beginning, then fills it in with CSV import/export and one PLT-style SaveResult path so you can see both input-side and solution-side workflows.
Plugin shape
A practical starter should do five things from day one:
- Keep the full ITcPlugin contract explicit, even when some callbacks still return .
- Cache host state during Initialize, especially RegRoot, LanguageId, UiScale, and ITonCut.
- Register .csv for and , then register .plt for .
- Populate IProjects, IMaterials, IFormats, and ISheets in LoadFile, then serialize the same source data again in SaveFile.
- Export transformed cut geometry from ISolutionSheet in SaveResult.
Keep the first code block declarative: one full starter declaration, no method bodies. That makes later sections easier to follow across Delphi and the native C++ ABI used by VS C++, C++Builder, and the C# starter's shim.
The Delphi and native C++ tabs below show the ABI-facing class shape. The C# project publishes no direct managed TonCut interfaces: its native shim owns this entire contract, while the shipped managed library contains only CSV parsing, validation, row data, and serialization.
Full starter declaration
type
TCsvPltStarterPlugin = class(TInterfacedObject, ITcPlugin)
private
FRegRoot: WideString;
FLanguageId: Integer;
FUiScale: Single;
FTonCut: ITonCut;
function ExportSolutionSheetAsPlt(
const aSolutionSheet: ISolutionSheet;
aCommands: TStrings
): Boolean;
private
function GetEngineVersion(): Integer; safecall;
function GetName(): WideString; safecall;
function GetVersion(): TTcpVersion; safecall;
function GetTitle(): WideString; safecall;
function GetProfessional(): Boolean; safecall;
function GetRegRoot(): WideString; safecall;
function GetLanguageId(): Integer; safecall;
procedure SetLanguageId(aLanguageId: Integer); safecall;
function GetUiScale(): Single; safecall;
procedure SetUiScale(aScale: Single); safecall;
function GetTonCut(): ITonCut; safecall;
function GetFeatures(): TTcpFeatures; safecall;
function GetPlotterInfo(): ITcPlotterInfo; safecall;
public
constructor Create;
function Initialize(const aRegRoot: WideString; aLanguageId: Integer; aTonCut: ITonCut): TTcpResult; safecall;
function Finalize(): TTcpResult; safecall;
function Configure(aWindow: THandle): TTcpResult; safecall;
function About(aWindow: THandle): TTcpResult; safecall;
function LoadConfig(const aKey, aVersion: WideString): TTcpResult; safecall;
function SaveConfig(const aKey: WideString): TTcpResult; safecall;
function BackupConfig(var aBackup: WideString): TTcpResult; safecall;
function RestoreConfig(const aBackup: WideString; aScope: TConfigurationParts): TTcpResult; safecall;
function EnumExtensions(aType: TTcpExtensionType; aEnumCb: ITcEnumExtensionsCallback): TTcpResult; safecall;
function IsMyFile(const aFileName: WideString): TTcpResult; safecall;
function LoadFile(aWindow: THandle; const aFileName: WideString; aDevices: IDevices; aMaterials: IMaterials; aBands: IBands; aProjects: IProjects; aFormats: IFormats; aSheets: ISheets): TTcpResult; safecall;
function SaveFile(aWindow: THandle; const aFileName: WideString; const aFormats: IFormats; const aSheets: ISheets): TTcpResult; safecall;
function SaveResult(aWindow: THandle; const aFileName: WideString; const aSolutionSheet: ISolutionSheet): TTcpResult; safecall;
function BatchSaveResult(aWindow: THandle; aSaveTarget: TTcpSaveTarget; const aPath: WideString; const aSolution: ISolution): TTcpResult; safecall;
function Plot(aWindow: THandle; const aSolutionSheet: ISolutionSheet): TTcpResult; safecall;
function BatchPlot(aWindow: THandle; const aSolution: ISolution): TTcpResult; safecall;
function SavePlot(aWindow: THandle; const aFileName: WideString; const aSolutionSheet: ISolutionSheet): TTcpResult; safecall;
function BatchSavePlot(aWindow: THandle; aSaveTarget: TTcpSaveTarget; const aPath: WideString; const aSolution: ISolution): TTcpResult; safecall;
end;
Entrypoint
The host loads the .plg binary with LoadLibrary, resolves TcpInitializeV2, and expects an initialized ITcPlugin result. The entrypoint should stay tiny: construct the plugin, call Initialize, and return null if initialization fails.
TcpInitializeV2
function TcpInitializeV2(
const aRegRoot: PChar;
aLanguageId: Integer;
aTonCut: ITonCut
): ITcPlugin; stdcall;
begin
Result := TCsvPltStarterPlugin.Create;
if Result.Initialize(aRegRoot, aLanguageId, aTonCut) <> cprOK then
Result := nil;
end;
The C# starter uses this same native C++ entrypoint in SDK/examples/csharp/native-shim/TonCutCsvShim.cpp; managed code never exports TcpInitializeV2.
Use aRegRoot as the plugin-specific configuration root in the Windows registry. Even when the starter does not persist anything yet, LoadConfig, SaveConfig, BackupConfig, and RestoreConfig should stay safe success paths.
Metadata and lifecycle
Keep the identity getters stable and boring. TonCut reads them often, and they are part of the plugin's public identity. Initialize should only cache host state and prepare lightweight defaults. Anything expensive belongs in the first real callback that needs it.
Metadata and lifecycle methods
constructor TCsvPltStarterPlugin.Create;
begin
inherited Create;
FUiScale := 1.0;
end;
function TCsvPltStarterPlugin.GetEngineVersion(): Integer;
begin
Result := cEngineVersion;
end;
function TCsvPltStarterPlugin.GetName(): WideString;
begin
Result := 'CsvPltStarter';
end;
function TCsvPltStarterPlugin.GetVersion(): TTcpVersion;
begin
Result := 1;
end;
function TCsvPltStarterPlugin.GetTitle(): WideString;
begin
Result := 'CSV + PLT Starter';
end;
function TCsvPltStarterPlugin.GetProfessional(): Boolean;
begin
Result := False;
end;
function TCsvPltStarterPlugin.GetRegRoot(): WideString;
begin
Result := FRegRoot;
end;
function TCsvPltStarterPlugin.GetLanguageId(): Integer;
begin
Result := FLanguageId;
end;
procedure TCsvPltStarterPlugin.SetLanguageId(aLanguageId: Integer);
begin
FLanguageId := aLanguageId;
end;
function TCsvPltStarterPlugin.GetUiScale(): Single;
begin
Result := FUiScale;
end;
procedure TCsvPltStarterPlugin.SetUiScale(aScale: Single);
begin
FUiScale := aScale;
end;
function TCsvPltStarterPlugin.GetTonCut(): ITonCut;
begin
Result := FTonCut;
end;
function TCsvPltStarterPlugin.GetFeatures(): TTcpFeatures;
begin
Result := [feImport, feExport, feSaveResultToFile];
end;
function TCsvPltStarterPlugin.GetPlotterInfo(): ITcPlotterInfo;
begin
Result := nil;
end;
function TCsvPltStarterPlugin.Initialize(
const aRegRoot: WideString;
aLanguageId: Integer;
aTonCut: ITonCut
): TTcpResult;
begin
FRegRoot := aRegRoot;
FLanguageId := aLanguageId;
FTonCut := aTonCut;
FUiScale := 1.0;
Result := cprOK;
end;
function TCsvPltStarterPlugin.Finalize(): TTcpResult;
begin
FTonCut := nil;
FRegRoot := '';
Result := cprOK;
end;
For the split C# project, this state and every lifecycle callback remain in the native shim. The managed CSV assembly receives no ITonCut pointer.
The starter still leaves configuration as a stub, but About is worth implementing early. A simple message box is an easy sanity check that TonCut can load the plugin, resolve the vtable correctly, and call back into your code.
Configuration and About
function TCsvPltStarterPlugin.Configure(aWindow: THandle): TTcpResult;
begin
Result := cprNotImplemented;
end;
function TCsvPltStarterPlugin.About(aWindow: THandle): TTcpResult;
begin
MessageBoxW(
aWindow,
PWideChar(WideString('CSV import/export plus single-sheet PLT result export.')),
PWideChar(GetTitle()),
MB_OK or MB_ICONINFORMATION
);
Result := cprOK;
end;
function TCsvPltStarterPlugin.LoadConfig(
const aKey, aVersion: WideString
): TTcpResult;
begin
Result := cprOK;
end;
function TCsvPltStarterPlugin.SaveConfig(
const aKey: WideString
): TTcpResult;
begin
Result := cprOK;
end;
function TCsvPltStarterPlugin.BackupConfig(
var aBackup: WideString
): TTcpResult;
begin
aBackup := '';
Result := cprOK;
end;
function TCsvPltStarterPlugin.RestoreConfig(
const aBackup: WideString;
aScope: TConfigurationParts
): TTcpResult;
begin
Result := cprOK;
end;
File registration and detection
EnumExtensions tells TonCut which file types belong to import, source export, and result export. IsMyFile only answers the import question. If the extension matches but the starter cannot confirm the CSV header yet, it returns so TonCut can keep asking other plugins before falling back to this one.
EnumExtensions and IsMyFile
function TCsvPltStarterPlugin.EnumExtensions(
aType: TTcpExtensionType;
aEnumCb: ITcEnumExtensionsCallback
): TTcpResult;
begin
case aType of
etImport,
etExport:
aEnumCb.Invoke(aType, '.csv', 'CSV source data');
etExportResults:
aEnumCb.Invoke(aType, '.plt', 'HP-GL / PLT result export');
end;
Result := cprOK;
end;
function TCsvPltStarterPlugin.IsMyFile(
const aFileName: WideString
): TTcpResult;
const
CHeader =
'RecordType,ProjectTitle,MaterialTitle,MaterialKind,MaterialWidth,MaterialThickness,Length,Width,Quantity,Identifier,Description';
var
lines: TStringList;
begin
if not SameText(ExtractFileExt(aFileName), '.csv') then
Exit(cprNo);
lines := TStringList.Create;
try
lines.LoadFromFile(aFileName, TEncoding.UTF8);
if (lines.Count > 0) and SameText(Trim(lines[0]), CHeader) then
Exit(cprOK);
Result := cprMaybe;
finally
lines.Free;
end;
end;
The native shim registers extensions and returns TonCut result codes. A project-specific bridge may send CSV text to CsvCodec.Parse and CsvCodec.HasRequiredHeader, but the managed library does not receive the callback interface.
Source data round-trip
LoadFile works on source data, not solution geometry. Use the collection factories such as IProjects.CreateProject, IMaterials.CreateMaterial, IFormats.CreateFormat, and ISheets.CreateSheet so the import stays non-interactive. The UI helpers on ITonCut are for interactive workflows, not automatic file loading.
The starter keeps the CSV codec simple on purpose. Production plugins usually add proper quoting, duplicate detection, validation, and material/project caches. What matters here is the plugin boundary: LoadFile fills host collections and SaveFile writes them back out.
LoadFile
function TCsvPltStarterPlugin.LoadFile(
aWindow: THandle;
const aFileName: WideString;
aDevices: IDevices;
aMaterials: IMaterials;
aBands: IBands;
aProjects: IProjects;
aFormats: IFormats;
aSheets: ISheets
): TTcpResult;
var
lines: TStringList;
fields: TArray<string>;
project: IProject;
material: IMaterial;
format: IFormat;
sheet: ISheet;
i: Integer;
begin
lines := TStringList.Create;
try
lines.LoadFromFile(aFileName, TEncoding.UTF8);
for i := 1 to lines.Count - 1 do
begin
if Trim(lines[i]) = '' then
Continue;
fields := lines[i].Split([',']);
if Length(fields) < 11 then
Exit(cprFailed);
material := aMaterials.CreateMaterial;
material.Title := fields[2];
if SameText(fields[3], '1D') then
material.Kind := mk1D
else
material.Kind := mk2D;
material.Width := StrToIntDef(fields[4], 0);
material.Thickness := StrToIntDef(fields[5], 0);
aMaterials.Add(material);
if SameText(fields[0], 'FORMAT') then
begin
project := aProjects.CreateProject;
project.Title := fields[1];
aProjects.Add(project);
format := aFormats.CreateFormat;
format.Project := project;
format.Material := material;
format.Length := StrToIntDef(fields[6], 0);
format.Width := StrToIntDef(fields[7], 0);
format.Quantity := StrToIntDef(fields[8], 1);
format.Identifier := fields[9];
format.Description := fields[10];
aFormats.Add(format);
end
else if SameText(fields[0], 'SHEET') then
begin
sheet := aSheets.CreateSheet;
sheet.Material := material;
sheet.Length := StrToIntDef(fields[6], 0);
sheet.Width := StrToIntDef(fields[7], 0);
sheet.Quantity := StrToIntDef(fields[8], 1);
sheet.Identifier := fields[9];
sheet.Description := fields[10];
aSheets.Add(sheet);
end;
end;
Result := cprOK;
finally
lines.Free;
end;
end;
In the C# starter, managed code can decode CSV into CsvRowRecord values, but native C++ must create and populate every TonCut object.
SaveFile
function TCsvPltStarterPlugin.SaveFile(
aWindow: THandle;
const aFileName: WideString;
const aFormats: IFormats;
const aSheets: ISheets
): TTcpResult;
var
lines: TStringList;
format: IFormat;
sheet: ISheet;
i: Integer;
function MaterialKindToText(aKind: TMaterialKind): WideString;
begin
case aKind of
mk1D: Result := '1D';
mk2D: Result := '2D';
else
Result := 'Unknown';
end;
end;
begin
lines := TStringList.Create;
try
lines.Add('RecordType,ProjectTitle,MaterialTitle,MaterialKind,MaterialWidth,MaterialThickness,Length,Width,Quantity,Identifier,Description');
for i := 0 to aFormats.Count - 1 do
begin
format := aFormats[i];
if format.Material = nil then
Continue;
lines.Add(Format(
'FORMAT,%s,%s,%s,%d,%d,%d,%d,%d,%s,%s',
[
format.Project.Title,
format.Material.Title,
MaterialKindToText(format.Material.Kind),
format.Material.Width,
format.Material.Thickness,
format.Length,
format.Width,
format.Quantity,
format.Identifier,
format.Description
]
));
end;
for i := 0 to aSheets.Count - 1 do
begin
sheet := aSheets[i];
if sheet.Material = nil then
Continue;
lines.Add(Format(
'SHEET,,%s,%s,%d,%d,%d,%d,%d,%s,%s',
[
sheet.Material.Title,
MaterialKindToText(sheet.Material.Kind),
sheet.Material.Width,
sheet.Material.Thickness,
sheet.Length,
sheet.Width,
sheet.Quantity,
sheet.Identifier,
sheet.Description
]
));
end;
lines.SaveToFile(aFileName, TEncoding.UTF8);
Result := cprOK;
finally
lines.Free;
end;
end;
For C#, the native shim extracts host data into plain CsvRowRecord values; the managed side serializes them with CsvCodec.Encode.
Result geometry export
SaveResult starts from ISolutionSheet, not from source pieces. This is where PLT-style exporters matter. The critical pattern is:
- Ask the solution sheet for its effective layout with
CalcEffectiveLayout. - Walk
SolutionCuts. - Pass every endpoint and every Bezier control point through
CalcPointF. - Only then emit machine coordinates or vector commands.
That transform sequence is the difference between exporting source geometry and exporting the placed, rotated, mirrored, solution-space geometry that the user actually sees.
SaveResult
function TCsvPltStarterPlugin.SaveResult(
aWindow: THandle;
const aFileName: WideString;
const aSolutionSheet: ISolutionSheet
): TTcpResult;
var
commands: TStringList;
begin
commands := TStringList.Create;
try
if not ExportSolutionSheetAsPlt(aSolutionSheet, commands) then
Exit(cprFailed);
commands.SaveToFile(aFileName, TEncoding.ASCII);
aSolutionSheet.IsSaved := True;
Result := cprOK;
finally
commands.Free;
end;
end;
The C# starter keeps SaveResult and PLT output entirely in native C++; the managed CSV assembly has no solution-sheet projection.
ExportSolutionSheetAsPlt helper
function TCsvPltStarterPlugin.ExportSolutionSheetAsPlt(
const aSolutionSheet: ISolutionSheet;
aCommands: TStrings
): Boolean;
var
sheetLength, sheetWidth: Cardinal;
marginLeft, marginTop, marginRight, marginBottom: Cardinal;
scale: Double;
i: Integer;
cut: ISolutionCut;
x0, y0, x1, y1, x2, y2, x3, y3: Double;
function ToPlotterUnit(const aValue: Double): Integer;
begin
Result := Round(aValue * scale);
end;
procedure Transform(var aX, aY: Double);
begin
aSolutionSheet.CalcPointF(aX, aY);
end;
begin
aCommands.Clear;
aSolutionSheet.CalcEffectiveLayout(
sheetLength, sheetWidth,
marginLeft, marginTop, marginRight, marginBottom
);
if (sheetLength = 0) or (sheetWidth = 0) then
Exit(False);
scale := 40.0 / 25.4;
aCommands.Add('IN;');
aCommands.Add('PA;');
for i := 0 to aSolutionSheet.SolutionCuts.Count - 1 do
begin
cut := aSolutionSheet.SolutionCuts[i];
case cut.Kind of
ckLine:
begin
x0 := cut.PosX; y0 := cut.PosY;
x3 := cut.TarX; y3 := cut.TarY;
Transform(x0, y0);
Transform(x3, y3);
aCommands.Add(Format('PU%d,%d;', [ToPlotterUnit(x0), ToPlotterUnit(y0)]));
aCommands.Add(Format('PD%d,%d;', [ToPlotterUnit(x3), ToPlotterUnit(y3)]));
end;
ckCurve:
begin
x0 := cut.PosX; y0 := cut.PosY;
x1 := cut.PosXp; y1 := cut.PosYp;
x2 := cut.TarXp; y2 := cut.TarYp;
x3 := cut.TarX; y3 := cut.TarY;
Transform(x0, y0);
Transform(x1, y1);
Transform(x2, y2);
Transform(x3, y3);
aCommands.Add(Format('PU%d,%d;', [ToPlotterUnit(x0), ToPlotterUnit(y0)]));
aCommands.Add(
Format(
'BZ%d,%d,%d,%d,%d,%d;',
[
ToPlotterUnit(x1), ToPlotterUnit(y1),
ToPlotterUnit(x2), ToPlotterUnit(y2),
ToPlotterUnit(x3), ToPlotterUnit(y3)
]
)
);
end;
end;
end;
aCommands.Add('PU;');
aCommands.Add('SP0;');
Result := True;
end;
The current VS C++ header still forward-declares ISolutionSheet and the solution-cut interfaces, so the Pascal tab is the full public-surface geometry example today. The algorithm itself is still language-neutral: transform every point through CalcPointF after you obtain the effective solution layout.
The starter still keeps the remaining output callbacks explicit:
Unsupported callbacks in the starter
function TCsvPltStarterPlugin.BatchSaveResult(
aWindow: THandle;
aSaveTarget: TTcpSaveTarget;
const aPath: WideString;
const aSolution: ISolution
): TTcpResult;
begin
Result := cprNotImplemented;
end;
function TCsvPltStarterPlugin.Plot(
aWindow: THandle;
const aSolutionSheet: ISolutionSheet
): TTcpResult;
begin
Result := cprNotImplemented;
end;
function TCsvPltStarterPlugin.BatchPlot(
aWindow: THandle;
const aSolution: ISolution
): TTcpResult;
begin
Result := cprNotImplemented;
end;
function TCsvPltStarterPlugin.SavePlot(
aWindow: THandle;
const aFileName: WideString;
const aSolutionSheet: ISolutionSheet
): TTcpResult;
begin
Result := cprNotImplemented;
end;
function TCsvPltStarterPlugin.BatchSavePlot(
aWindow: THandle;
aSaveTarget: TTcpSaveTarget;
const aPath: WideString;
const aSolution: ISolution
): TTcpResult;
begin
Result := cprNotImplemented;
end;
Those unsupported callbacks also remain native-shim methods in the split C# starter.
For the full SDK layout, manuals, and ready-to-open project files, use /sdk.