Skip to content

Migrating from Legacy Scripting

This guide helps you migrate existing Mattercraft projects from the legacy JSDoc-based scripting system to the modern decorator-based approach.

  1. Update your dependencies - Ensure all @zcomponent/* packages are updated to the latest version
  2. Back up your project - Make a copy before making changes
  3. Migrate incrementally - You can have legacy and modern scripts in the same project during migration
Legacy (JSDoc) Modern (Decorators)
/** @zbehavior */ @zBehavior({ icon: 'favorite' })
/** @zcomponent */ @zComponent({ icon: 'favorite' })
/** @zcontext */ @zContext()
/** @zicon favorite */ @zBehavior({ icon: 'favorite' })
Legacy (JSDoc) Modern (Decorators)
/** @zui */ Not needed - properties are visible by default
/** @zui */ /** @zgroup Settings */ @zUI({ group: 'Settings' })
/** @zui */ /** @ztype proportion */ @zUI({ type: 'proportion' })
/** @zdefault 0 */ Set value directly: property = 0
new Observable(value) Plain property with @zObserve()

Public properties are now visible in the editor by default. Use @zUI() only when you need to customize appearance (grouping, type hints). Use @zIgnore() to hide a property.

Legacy Pattern Modern Decorator
this.register(useOnBeforeRender(ctx), fn) @zOnBeforeRender() on method
this.register(useOnAfterRender(ctx), fn) @zOnAfterRender() on method
registerLoadable(ctx, fn()) @zLoad() on async method
this.register(this.instance.onClick, fn) @zRegister('onClick') on method
registerBehaviorRunAtDesignTime(Class) @zBehavior({ runAtDesignTime: true })
import { Behavior, ContextManager } from "@zcomponent/core";
import { Box } from "@zcomponent/three/lib/components/meshes/Box";
import { default as Scene } from "./Scene.zcomp";
interface ConstructionProps {}
/**
* @zbehavior
* @zicon rotate_right
*/
export class RotateBehavior extends Behavior<Box> {
protected zcomponent = this.getZComponentInstance(Scene);
constructor(
contextManager: ContextManager,
instance: Box,
protected constructorProps: ConstructionProps
) {
super(contextManager, instance);
this.register(useOnBeforeRender(contextManager), dt => {
this.instance.element.rotation.y += 0.01 * dt;
});
this.register(this.instance.onClick, () => {
console.log('Clicked!');
});
}
}
import { Behavior, zBehavior, zOnBeforeRender, zRegister } from '@zcomponent/core';
import { Box } from '@zcomponent/three/lib/components/meshes/Box';
@zBehavior({ icon: 'rotate_right' })
export class RotateBehavior extends Behavior<Box> {
@zOnBeforeRender()
private _rotate(deltaTime: number) {
this.instance.element.rotation.y += 0.01 * deltaTime;
}
@zRegister('onClick')
private _handleClick() {
console.log('Clicked!');
}
}
  1. Class decorator - Replace /** @zbehavior */ with @zBehavior({ icon: '...' })
  2. Frame loop - Replace this.register(useOnBeforeRender(...)) with @zOnBeforeRender() method decorator
  3. Events - Replace this.register(event, fn) with @zRegister('eventName') method decorator
  4. Remove constructor boilerplate - Most logic moves from constructor to decorated methods
import { ContextManager, registerLoadable } from "@zcomponent/core";
import { Group } from "@zcomponent/three/lib/components/Group";
import * as THREE from "three";
interface ConstructorProps {}
/**
* @zcomponent
* @zicon favorite
*/
export class MySphere extends Group {
constructor(contextManager: ContextManager, constructorProps: ConstructorProps) {
super(contextManager, constructorProps);
registerLoadable(contextManager, this._load());
}
private async _load() {
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(),
new THREE.MeshBasicMaterial()
);
this.element.add(mesh);
}
}
import { ContextManager, zComponent, zLoad } from '@zcomponent/core';
import { Group } from '@zcomponent/three/lib/components/Group';
import * as THREE from 'three';
@zComponent({ icon: 'favorite' })
export class MySphere extends Group {
@zLoad()
private async _load() {
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(),
new THREE.MeshBasicMaterial()
);
this.element.add(mesh);
}
}
  1. Class decorator - Replace /** @zcomponent */ with @zComponent({ icon: '...' })
  2. Async loading - Replace registerLoadable(ctx, fn()) with @zLoad() on the method
  3. Cleaner constructor - Remove boilerplate from constructor
/**
* @zui
* @zdefault 0
* @zgroup Appearance
*/
public metalness = new Observable(0);
constructor(...) {
this.register(this.metalness, value => {
this.material.metalness = value;
});
}
// Simple property - no decorators needed for basic visibility
public metalness = 0;
// With reactive updates
@zObserve((value, instance) => {
instance.material.metalness = value;
})
public metalness = 0;
// With UI customization (grouping, type hints)
@zUI({ group: 'Appearance', type: 'proportion' })
@zObserve((value, instance) => {
instance.material.metalness = value;
})
public metalness = 0;
  1. Start with behaviors - They’re usually simpler and give you practice with the new patterns
  2. Test incrementally - Migrate one script at a time and test after each change
  3. Use autocomplete - The modern system has excellent TypeScript support
  4. Check the console - Decorator errors will show helpful messages