Skip to content

Command

mlight lee edited this page Feb 10, 2026 · 4 revisions

Creating a Custom Command in CAD-Viewer

This page explains how to create a custom command in CAD-Viewer and register it so that it can be triggered by the user.

Overview

Commands are the primary way users interact with the CAD-Viewer. Each command represents a specific operation, such as drawing a line, creating a circle, zooming, or selecting objects.

The system provides a command stack (AcEdCommandStack) to manage commands in groups, look them up by name, and handle their lifecycle events through the editor.

Command Lifecycle

Each command follows a well-defined lifecycle managed by AcEdCommand:

  1. The command is triggered via trigger()
  2. The commandWillStart event is fired
  3. The execute() method is called with the current context
  4. The commandEnded event is fired (always, even if an error occurs)

Internally, this lifecycle is implemented as follows:

async trigger(context: AcApContext) {
  try {
    this.onCommandWillStart(context)
    context.view.editor.events.commandWillStart.dispatch({ command: this })
    await this.execute(context)
  } finally {
    context.view.editor.events.commandEnded.dispatch({ command: this })
    this.onCommandEnded(context)
  }
}

Key Guarantees

  • commandWillStart is always fired before execute()

  • commandEnded is always fired after execution, even if the command throws or is cancelled

  • Commands can hook into lifecycle logic by overriding:

    • onCommandWillStart(context)
    • onCommandEnded(context)

Creating a Command

All commands should extend the abstract AcEdCommand class:

import {
  AcEdCommand,
  AcApContext,
  AcApDocManager,
  AcEdPromptPointOptions,
  AcEdPromptDistanceOptions
} from '@mlightcad/cad-simple-viewer'

import { AcDbCircle } from '@mlightcad/data-model'

export class AcApCircleCmd extends AcEdCommand {
  async execute(context: AcApContext) {
    // Prompt for center point
    const centerPrompt = new AcEdPromptPointOptions(
      'Specify center point of circle:'
    )
    const center = await AcApDocManager.instance.editor.getPoint(centerPrompt)

    // Prompt for radius
    const radiusPrompt = new AcEdPromptDistanceOptions(
      'Specify radius of circle:'
    )
    const radius = await AcApDocManager.instance.editor.getDistance(radiusPrompt)

    // Create circle entity and add it to model space
    const db = context.doc.database
    const circle = new AcDbCircle(center, radius)
    db.tables.blockTable.modelSpace.appendEntity(circle)
  }
}

Notes

  • execute() receives the current AcApContext, which contains:

    • context.view
    • context.doc
  • If you are already inside a command, always prefer:

    context.view.editor
    

    instead of accessing the global editor instance.

Listening to Command Lifecycle Events

Command lifecycle events are exposed by the editor, not by the command itself.

Available Editor Events

public readonly events = {
  sysVarChanged: new AcCmEventManager<AcDbSysVarEventArgs>(),
  /** Fired just before the command starts executing */
  commandWillStart: new AcCmEventManager<AcEdCommandEventArgs>(),
  /** Fired after the command finishes executing */
  commandEnded: new AcCmEventManager<AcEdCommandEventArgs>()
}

Getting the AcEditor Instance

Depending on where you are in the application:

From anywhere (global access)

const editor =
  AcApDocManager.instance.curView.editor

Inside a command execution

const editor = context.view.editor

Example: Listening to Command Events

const editor = AcApDocManager.instance.curView.editor

editor.events.commandWillStart.addEventListener(({ command }) => {
  console.log('Command will start:', command.globalName)
})

editor.events.commandEnded.addEventListener(({ command }) => {
  console.log('Command ended:', command.globalName)
})

Typical Use Cases

  • Tracking command history
  • Updating UI state (toolbars, panels)
  • Locking or unlocking interactions
  • Implementing analytics or telemetry
  • Emulating ObjectARX commandWillStart / commandEnded behavior

Command Access Mode

AcEdCommand now supports a minimum access mode requirement, allowing commands to declare what document access level they need.

Open Modes

CAD-Viewer supports three document open modes:

export enum AcEdOpenMode {
  /** Read-only mode */
  Read = 0,
  /** Review mode (compatible with Read) */
  Review = 4,
  /** Write mode (compatible with Review and Read) */
  Write = 8
}

Higher-value modes are compatible with lower-value modes:

  • WriteReviewRead

You can access the current document’s open mode via:

context.doc.openMode

or

AcApDocument.openMode

Setting Command Mode

Each command can declare the minimum required access mode:

import { AcEdOpenMode } from '@mlightcad/cad-simple-viewer'

export class AcApCircleCmd extends AcEdCommand {
  constructor() {
    super()
    this.mode = AcEdOpenMode.Write
  }

  async execute(context: AcApContext) {
    // write operations
  }
}

Mode Property

get mode(): AcEdOpenMode
set mode(value: AcEdOpenMode)

Behavior

  • A command can only execute if:

    document.openMode >= command.mode
    
  • Examples:

    • A Write command cannot run in Read mode
    • A Review command can run in Write mode
    • A Read command runs in all modes

This enables:

  • Safe read-only viewers
  • Review-only workflows
  • Strict write protection for editing commands

Registering a Command

Commands are registered through AcApDocManager.instance.commandManager.

import { AcApDocManager } from '@mlightcad/cad-simple-viewer'
import { AcApCircleCmd } from './AcApCircleCmd'

const register = AcApDocManager.instance.commandManager

const circleCommand = new AcApCircleCmd()
circleCommand.globalName = 'CIRCLE'
circleCommand.localName = 'CIRCLE'

register.addCommand(
  'USER',
  circleCommand.globalName,
  circleCommand.localName,
  circleCommand
)

Notes

  • globalName must be unique within the command group

  • localName is user-facing and can be localized

  • Command groups:

    • ACAD – system commands
    • USER – custom commands

Triggering a Command

Once registered, a command can be executed programmatically:

AcApDocManager.instance.sendStringToExecute('CIRCLE')

Complete Workflow

  1. Create a command by extending AcEdCommand
  2. (Optional) Set the command’s required access mode
  3. Register it with commandManager
  4. (Optional) Listen to lifecycle events via AcEditor if needed
  5. Trigger the command by name or programmatically

Localizing a Custom Command

In https://github.com/mlightcad/cad-simple-viewer-example you can find a full example demonstrating custom commands with localization support.

Clone this wiki locally