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:

  1. Keep the full ITcPlugin contract explicit, even when some callbacks still return
    .
  2. Cache host state during Initialize, especially RegRoot, LanguageId, UiScale, and ITonCut.
  3. Register .csv for
    and
    , then register .plt for
    .
  4. Populate IProjects, IMaterials, IFormats, and ISheets in LoadFile, then serialize the same source data again in SaveFile.
  5. 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.

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.

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:

  1. Ask the solution sheet for its effective layout with CalcEffectiveLayout.
  2. Walk SolutionCuts.
  3. Pass every endpoint and every Bezier control point through CalcPointF.
  4. 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 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.

Was this page helpful?