Migrating to the New Variable System

Important

This guide is relevant if you have a URCap that was created using APIs introduced in PolyScope X 10.11 or older. Starting from PolyScope X 10.12, these APIs are deprecated.

This guide explains how to migrate from the deprecated URVariable class-based variable system to the new interface-based variable system. The new system provides better type safety, clearer separation between declarations and references, and improved support for variable state management.

Overview of Changes

The legacy variable system used class-based symbols (URVariable, URSymbol) with an inheritance hierarchy. The new system replaces these with plain TypeScript interfaces that provide clearer semantics:

Old (Deprecated)

New (Recommended)

Purpose

URVariable class

VariableDeclaration interface

Declaring a new variable

URVariable with reference=true

VariableReference interface

Referencing an existing variable

VariableValueType enum

VARIABLE_VALUE_TYPES constant array

Variable type definition

isVariable() guard

isVariableDeclaration(), isVariableReference()

Type checking

N/A

VariableDescription, VariableDescriptionWithReference

UI/state consumption

Import Changes

Old Imports (Deprecated)

import {
  URVariable,
  VariableValueType,
  isVariable,
} from '@universal-robots/contribution-api';

Key Interface Definitions

VariableDeclaration

Use this interface when your node declares (creates) a new variable:

interface VariableDeclaration {
  id: string; // Unique identifier
  name: string; // Variable name
  valueType: (typeof VARIABLE_VALUE_TYPES)[number]; // e.g., 'integer', 'float', 'string'
  _IDENTIFIER: 'VariableDeclaration'; // Type discriminator
}

VariableReference

Use this interface when your node references an existing variable:

interface VariableReference {
  id: string; // ID of the referenced variable
  _IDENTIFIER: 'VariableReference'; // Type discriminator
}

VariableDescription and VariableDescriptionWithReference

These interfaces describe the current state of a variable for use in the UI. They are returned by the VariableService.variables() observable:

interface VariableDescription {
  name: string;
  valueType: (typeof VARIABLE_VALUE_TYPES)[number];
  // ... additional state properties
}

type VariableDescriptionWithReference = VariableDescription & {
  reference: VariableReference;
};

Important

VariableDescription objects should be treated as immutable and should not be saved in node parameters, as their properties are subject to external changes.

Variable Value Types

Old Approach (Deprecated)

import { VariableValueType } from '@universal-robots/contribution-api';

const myType = VariableValueType.INTEGER; // Using enum

Creating Variables

Old Approach (Deprecated)

import {
  ProgramBehaviorAPI,
  VariableValueType,
} from '@universal-robots/contribution-api';

// In a behavior worker, use the SymbolService
const api = new ProgramBehaviorAPI(self);

// Creating a new variable declaration (returns URVariable)
const variable = await api.symbolService.generateVariable(
  'myVar',
  VariableValueType.INTEGER
);

// Creating a variable reference (URVariable with reference=true)
const reference = await api.symbolService.generateVariable(
  'existingVar',
  VariableValueType.INTEGER
);
reference.reference = true;

New Approach (Recommended)

import { ProgramBehaviorAPI } from '@universal-robots/contribution-api';

// In a behavior worker, use the VariableService
const api = new ProgramBehaviorAPI(self);

// Create a new variable declaration (returns VariableDeclaration)
const declaration = await api.symbolService.createVariable('myVar', 'integer');

// To reference an existing variable, store its VariableReference
// and use getVariableDescription to get the current state
const description = await api.symbolService.getVariableDescription(
  variableReference
);

Migrating Node Parameters

If your node stores URVariable objects in its parameters, you should migrate to use VariableDeclaration or VariableReference depending on whether the variable is declared or referenced.

Example Node Migration

Old Node Definition

import { ProgramNode, URVariable } from '@universal-robots/contribution-api';

export interface MyNode extends ProgramNode {
  type: 'my-node';
  parameters: {
    variable: URVariable; // Could be declaration or reference
  };
}

New Node Definition

For nodes that declare variables:

import {
  ProgramNode,
  VariableDeclaration,
} from '@universal-robots/contribution-api';

export interface MyNode extends ProgramNode {
  type: 'my-node';
  version: '2.0.0';
  parameters: {
    variable: VariableDeclaration;
  };
}

For nodes that reference existing variables:

import {
  ProgramNode,
  VariableReference,
} from '@universal-robots/contribution-api';

export interface MyNode extends ProgramNode {
  type: 'my-node';
  version: '2.0.0';
  parameters: {
    variableRef: VariableReference;
  };
}

Using the VariableService API

The VariableService provides methods for working with variables in behavior workers:

import { ProgramBehaviorAPI } from '@universal-robots/contribution-api';

const api = new ProgramBehaviorAPI(self);

// Get all available variables (returns Observable)
api.symbolService.variables().subscribe((variables) => {
  // variables is Array<VariableDescriptionWithReference>
});

// Create a new variable declaration
const declaration = await api.symbolService.createVariable('myVar', 'integer');

// Get variable description from a reference
const description = await api.symbolService.getVariableDescription(myReference);

// Upgrade a legacy URVariable to the new model
const upgraded = await api.symbolService.upgradeURVariable(legacyVariable);

Upgrading Legacy Nodes

When loading programs saved with the old variable system, use the upgradeURVariable method in your upgradeNode function:

import {
  ProgramBehaviorAPI,
  ProgramBehaviors,
  VariableDeclaration,
  VariableDescriptionWithReference,
} from '@universal-robots/contribution-api';

const behaviors: ProgramBehaviors<MyNode> = {
  // ... other behaviors

  upgradeNode: async (node: unknown): Promise<MyNode> => {
    const oldNode = node as any;

    // Check if this is an old version with URVariable
    if (oldNode.version === '1.0.0' && oldNode.parameters?.variable) {
      const api = new ProgramBehaviorAPI(self);

      // Upgrade the legacy URVariable
      const upgraded = await api.symbolService.upgradeURVariable(
        oldNode.parameters.variable
      );

      // upgraded is either:
      // - VariableDeclaration (if variable.reference was false)
      // - VariableReference (if variable.reference was true)

      const newNode: MyNode = {
        ...oldNode,
        version: '2.0.0',
        parameters: {
          variable: upgraded as VariableDeclaration,
        },
      };

      return newNode;
    }

    return node as MyNode;
  },
};

Understanding upgradeURVariable Results

The upgradeURVariable method returns different types based on the input:

Input

Output

URVariable with reference=false

VariableDeclaration (new declaration)

URVariable with reference=true

VariableReference (existing variable)

Already upgraded variable

Returns as-is

Type Guards

Old Approach (Deprecated)

import { isVariable, URVariable } from '@universal-robots/contribution-api';

if (isVariable(item)) {
  // item is URVariable
}

New Approach (Recommended)

import {
  isVariableDeclaration,
  isVariableReference,
  VariableDeclaration,
  VariableReference,
} from '@universal-robots/contribution-api';

if (isVariableDeclaration(item)) {
  // item is VariableDeclaration
  console.log(item.name, item.valueType);
}

if (isVariableReference(item)) {
  // item is VariableReference
  console.log(item.id);
}

Complete Migration Example

Here’s a complete example showing how to migrate a program node from the old to the new variable system:

Before Migration

/// <reference lib="webworker" />
import {
  ProgramBehaviorAPI,
  ProgramBehaviors,
  ProgramNode,
  registerProgramBehavior,
  ScriptBuilder,
  URVariable,
  VariableValueType,
} from '@universal-robots/contribution-api';

interface OldMyNode extends ProgramNode {
  type: 'my-custom-node';
  parameters: {
    variable: URVariable;
    value: string;
  };
}

const behaviors: ProgramBehaviors<OldMyNode> = {
  factory: async (): Promise<OldMyNode> => {
    const api = new ProgramBehaviorAPI(self);
    const variable = await api.symbolService.generateVariable(
      'myVar',
      VariableValueType.INTEGER
    );

    return {
      type: 'my-custom-node',
      allowsChildren: false,
      parameters: {
        variable,
        value: '0',
      },
    };
  },

  generateCodeBeforeChildren: async (
    node: OldMyNode
  ): Promise<ScriptBuilder> => {
    const builder = new ScriptBuilder();
    const { variable, value } = node.parameters;

    if (variable.reference) {
      builder.assign(variable.name, value);
    } else {
      builder.globalVariable(variable.name, value);
    }

    return builder;
  },
};

registerProgramBehavior(behaviors);

After Migration

/// <reference lib="webworker" />
import {
  ProgramBehaviorAPI,
  ProgramBehaviors,
  ProgramNode,
  registerProgramBehavior,
  ScriptBuilder,
  VariableDeclaration,
  VariableReference,
  isVariableDeclaration,
} from '@universal-robots/contribution-api';

interface NewMyNode extends ProgramNode {
  type: 'my-custom-node';
  version: '2.0.0';
  parameters: {
    variable: VariableDeclaration;
    value: string;
  };
}

// Type for the old node version (for upgrade purposes)
interface OldMyNode extends ProgramNode {
  type: 'my-custom-node';
  version?: undefined;
  parameters: {
    variable: any; // URVariable
    value: string;
  };
}

const behaviors: ProgramBehaviors<NewMyNode> = {
  factory: async (): Promise<NewMyNode> => {
    const api = new ProgramBehaviorAPI(self);
    const variable = await api.symbolService.createVariable('myVar', 'integer');

    return {
      type: 'my-custom-node',
      version: '2.0.0',
      allowsChildren: false,
      parameters: {
        variable,
        value: '0',
      },
    };
  },

  generateCodeBeforeChildren: async (
    node: NewMyNode
  ): Promise<ScriptBuilder> => {
    const builder = new ScriptBuilder();
    const { variable, value } = node.parameters;

    // With the new system, declarations always create new variables
    builder.globalVariable(variable.name, value);

    return builder;
  },

  upgradeNode: async (node: unknown): Promise<NewMyNode> => {
    const oldNode = node as OldMyNode;

    // If already upgraded, return as-is
    if ((node as any).version === '2.0.0') {
      return node as NewMyNode;
    }

    // Upgrade from old URVariable format
    const api = new ProgramBehaviorAPI(self);
    const upgraded = await api.symbolService.upgradeURVariable(
      oldNode.parameters.variable
    );

    return {
      type: 'my-custom-node',
      version: '2.0.0',
      allowsChildren: false,
      parameters: {
        variable: upgraded as VariableDeclaration,
        value: oldNode.parameters.value,
      },
    };
  },
};

registerProgramBehavior(behaviors);

Summary

  1. Replace URVariable class with VariableDeclaration (for declarations) or VariableReference (for references)

  2. Replace VariableValueType enum with VARIABLE_VALUE_TYPES constant array

  3. Use upgradeURVariable in your upgradeNode function to migrate saved programs

  4. Use the VariableService API (createVariable, getVariableDescription, variables()) for variable operations

  5. Use the new type guards (isVariableDeclaration, isVariableReference) for type checking

The new system provides better type safety and clearer semantics while maintaining backwards compatibility through the upgrade mechanism.