Skip to content

MXCAD McDbCustomEntity Custom Entity

In MXCAD, developers can create custom entities that meet specific business needs by inheriting the McDbCustomEntity class. Custom entities allow you to define unique data structures, rendering logic, and interaction behaviors. The following section details the purposes, invocation timing, and general implementation specifications of core methods in custom entities.

Click McDbCustomEntity to view detailed property and method descriptions.

Data Persistence Methods

Custom entities need to correctly serialize and deserialize their internal data during file saving, reading, or copying.

1. dwgInFields(filter: IMcDbDwgFiler): boolean

Purpose: Reads custom entity data from a file or stream to restore the entity state.

Invocation Timing: Scenarios requiring data loading from storage media, such as opening drawings, inserting blocks, or copying entities.

Implementation Points:

  • Use the filter object to read data by field name (e.g., readPoint, readString, readInt).
  • Assign the read data to the entity's private attributes.
  • For complex structures (e.g., matrices), manually parse and reconstruct them (e.g., parse a JSON string into a 2D array and fill the matrix).
  • Return true on success, false on failure.
ts
public dwgInFields(filter: IMcDbDwgFiler): boolean {
  this.pt1 = filter.readPoint("pt1").val;
  this.pt2 = filter.readPoint("pt2").val;
  const matrixStr = filter.readString("matrix").val;
  const matrixData = JSON.parse(matrixStr) as number[][];

  // Reconstruct matrix data
  for (let i = 0; i < matrixData.length; i++) {
    for (let j = 0; j < matrixData[i].length; j++) {
      this.matrix.setData(i, j, matrixData[i][j]);
    }
  }
  return true;
}

2. dwgOutFields(filter: IMcDbDwgFiler): boolean

Purpose: Writes the current state of the custom entity to a file or stream for persistent storage.

Invocation Timing: Scenarios requiring memory data to be written to storage, such as saving drawings, copying entities, or exporting blocks.

Implementation Points:

  • Use the filter object to write data by field name (e.g., writePoint, writeString, writeInt).
  • Complex data types (e.g., matrices) must be converted into a serializable format (e.g., JSON strings).
  • Return true on success, false on failure.
ts
public dwgOutFields(filter: IMcDbDwgFiler): boolean {
  filter.writePoint("pt1", this.pt1);
  filter.writePoint("pt2", this.pt2);

  const matrix: number[][] = [];
  for (let i = 0; i < 4; i++) {
    matrix[i] = [];
    for (let j = 0; j < 4; j++) {
      matrix[i][j] = this.matrix.getData(i, j);
    }
  }
  filter.writeString("matrix", JSON.stringify(matrix));
  return true;
}

Interaction and Editing Methods

Custom entities must support intuitive editing by users via Grip Points.

1. getGripPoints(): McGePoint3dArray

Purpose: Returns the collection of edit grip point positions displayed for the entity in the view.

Invocation Timing: Called by the system to retrieve operable grips when the entity is selected.

Implementation Points:

  • Return a McGePoint3dArray object containing all key control points.
  • The grip order corresponds to the index iIndex in moveGripPointsAt (e.g., index 0 corresponds to the start point, index 1 to the end point).
  • Typically returns key geometric positions such as start point, end point, and center point.
ts
// Example using a custom line entity
public getGripPoints(): McGePoint3dArray {
  const points = new McGePoint3dArray();
  points.append(this.pt1); // Index 0
  points.append(this.pt2); // Index 1
  return points;
}

2. moveGripPointsAt(iIndex: number, dXOffset: number, dYOffset: number, dZOffset: number): void

Purpose: Handles coordinate offsets after a user drags a grip, updating the entity's geometric data.

Invocation Timing: Triggered when the user drags a grip and releases the mouse.

Implementation Points:

  • Determine which grip is moved based on iIndex.
  • Apply offsets (dXOffset, dYOffset, dZOffset) to the corresponding attributes.
  • Must call this.assertWrite() to ensure the entity is in a writable state.
  • No return value; directly modifies the internal state.
ts
// Example using a custom line entity
public moveGripPointsAt(iIndex: number, dXOffset: number, dYOffset: number, dZOffset: number) {
  this.assertWrite(); // Mark as writable
  if (iIndex === 0) {
    this.pt1.x += dXOffset;
    this.pt1.y += dYOffset;
    this.pt1.z += dZOffset;
  } else if (iIndex === 1) {
    this.pt2.x += dXOffset;
    this.pt2.y += dYOffset;
    this.pt2.z += dZOffset;
  }
}

Rendering and Display Methods

Custom entities must define their visual representation in the view.

1. worldDraw(draw: MxCADWorldDraw): void

Purpose: Draws the graphical representation of the entity in the view.

Invocation Timing: Automatically triggered after view refresh, entity creation, or transformation operations.

Implementation Points:

  • Use the draw object to draw basic primitives (e.g., lines, circles, polylines) combined into custom graphics.
  • Transformation matrices, colors, line types, and other attributes can be applied.
  • Do not modify entity data in this method; it is responsible for visualization only.
ts
// Example using a custom line entity
public worldDraw(draw: MxCADWorldDraw): void {
  // Create a temporary base entity for drawing
  const line = new McDbLine(this.pt1, this.pt2);
  // Apply custom transformation
  line.transformBy(this.matrix);
  // Delegate to system for drawing
  draw.drawEntity(line);
}

TIP

Note: Since instances are rebuilt on every redraw, ensure all state is saved in entity attributes, not in local variables or external references.

Geometric Calculation and Transformation Methods

Custom entities must support spatial transformations and boundary calculations to be compatible with general CAD system operations.

1. getBoundingBox(): { minPt: McGePoint3d; maxPt: McGePoint3d; ret: boolean }

Purpose: Calculates the minimum bounding box of the entity.

Invocation Timing: Scenarios such as zooming view to entity, selection set judgment, collision detection, etc.

Implementation Points:

  • Return an object containing minPt (minimum corner point), maxPt (maximum corner point), and ret (success status).
  • Consider the spatial range of all geometric parts of the entity (including transformed coordinates).
  • If the entity has no valid geometry, ret should be set to false.
ts
public getBoundingBox(): { minPt: McGePoint3d; maxPt: McGePoint3d; ret: boolean } {
  const minX = Math.min(this.pt1.x, this.pt2.x);
  const minY = Math.min(this.pt1.y, this.pt2.y);
  const maxX = Math.max(this.pt1.x, this.pt2.x);
  const maxY = Math.max(this.pt1.y, this.pt2.y);

  return {
    minPt: new McGePoint3d(minX, minY, 0),
    maxPt: new McGePoint3d(maxX, maxY, 0),
    ret: true
  };
}

2. transformBy(mat: McGeMatrix3d): boolean

Purpose: Applies an affine transformation (translation, rotation, scaling, mirroring, etc.) to the entity.

Invocation Timing: When executing editing commands such as MOVE, ROTATE, SCALE, MIRROR, etc.

Implementation Points:

  • Multiply the incoming matrix mat with the entity's current transformation matrix (usually using preMultBy for pre-multiplication or postMultBy for post-multiplication, depending on coordinate system requirements).
  • Call this.assertWrite() to mark the entity as writable.
  • Return true on success, false on failure.
ts
// Example using a custom line entity
public transformBy(mat: McGeMatrix3d): boolean {
  // Apply external transformation matrix to the internally stored matrix
  this.matrix = this.matrix.preMultBy(mat);
  this.assertWrite();
  return true;
}

Auxiliary Methods and Best Practices

1. Constructor and create Method

  • The constructor is used to initialize the instance.
  • create(imp: any) is a factory method used to create new instances during deserialization or cloning and must be overridden.
ts
// Example using a custom line entity

constructor(imp?: any) {
  super(imp);
}

public create(imp: any) {
  return new McDbTestLineCustomEntity(imp);
}

2. Type Identification

Override getTypeName(): string to return a unique type name for system identification and debugging.

ts
// Custom entity type name
public getTypeName(): string {
  return "McDbTestLineCustomEntity";
}

3. Data Accessors

Provide getXXX() and setXXX() methods for safe external access to private attributes, and call assertWrite() within the setter.

ts
// Example using a custom line entity

public setPoint1(pt1: McGePoint3d) {
  this.assertWrite();
  this.pt1 = pt1.clone();
}

public getPoint1() {
  return this.pt1;
}

TIP

Other method access names can also be set, as long as they ensure access to public methods.

Custom Entity Registration

After creating the custom entity according to the steps above, you need to register the custom entity in the mxcad object. A custom entity only needs to be registered once in the mxcad object, and the class name of the custom entity must be unique. If the same custom entity class name is registered multiple times, the console will throw an error: "MxCADError:already has this class name McDbTestLineCustomEntity". Therefore, we can perform registration during mxcad initialization.

ts
import { MxCpp } from "mxcad";

// McDbTestLineCustomEntity is the custom entity class name you created
const mxcad = MxCpp.getCurrentMxCAD();
mxcad.on("init", () => {
  new McDbTestLineCustomEntity().rxInit();
});

Functional Practice

The following example demonstrates the complete creation of a custom entity by creating a custom line entity.

Inherit McDbCustomEntity to Define the Entity

ts
import {
  IMcDbDwgFiler,
  McDbCustomEntity,
  McDbLine,
  McGePoint3d,
  McGePoint3dArray,
  MxCADUiPrPoint,
  MxCADWorldDraw,
  MxCpp,
  McGeMatrix3d,
} from  "mxcad";

class McDbTestLineCustomEntity extends McDbCustomEntity {
  private pt1: McGePoint3d = new McGePoint3d();
  private pt2: McGePoint3d = new McGePoint3d();
  private matrix: McGeMatrix3d = new McGeMatrix3d();

  constructor(imp?: any) {
    super(imp);
  }

  public create(imp: any) {
    return new McDbTestLineCustomEntity(imp);
  }

  public getTypeName(): string {
    return  "McDbTestLineCustomEntity";
  }

  public dwgInFields(filter: IMcDbDwgFiler): boolean {
    this.pt1 = filter.readPoint("pt1").val;
    this.pt2 = filter.readPoint("pt2").val;
    const matrix = filter.readString("matrix").val;
    const matrixData = JSON.parse(matrix) as number[][];
    for (let index = 0; index < matrixData.length; index++) {
      const element = matrixData[index];
      for (let j = 0; j < element.length; j++) {
        const data = element[j];
        this.matrix.setData(index, j, data);
      }
    }
    return true;
  }

  public dwgOutFields(filter: IMcDbDwgFiler): boolean {
    filter.writePoint("pt1", this.pt1);
    filter.writePoint("pt2", this.pt2);
    const matrix: number[][] = [];
    for (let i = 0; i < 4; i++) {
      matrix[i] = [];
      for (let j = 0; j < 4; j++) {
        const data = this.matrix.getData(i, j);
        matrix[i][j] = data;
      }
    }
    filter.writeString("matrix", JSON.stringify(matrix));
    return true;
  }

  public moveGripPointsAt(
    iIndex: number,
    dXOffset: number,
    dYOffset: number,
    dZOffset: number,
  ) {
    this.assertWrite();
    if (iIndex == 0) {
      this.pt1.x += dXOffset;
      this.pt1.y += dYOffset;
      this.pt1.z += dZOffset;
    } else if (iIndex == 1) {
      this.pt2.x += dXOffset;
      this.pt2.y += dYOffset;
      this.pt2.z += dZOffset;
    }
  }

  public getGripPoints(): McGePoint3dArray {
    let ret = new McGePoint3dArray();
    ret.append(this.pt1);
    ret.append(this.pt2);
    return ret;
  }

  public worldDraw(draw: MxCADWorldDraw): void {
    let tmpline = new McDbLine(this.pt1, this.pt2);
    tmpline.transformBy(this.matrix);
    draw.drawEntity(tmpline);
  }

  public setPoint1(pt1: McGePoint3d) {
    this.assertWrite();
    this.pt1 = pt1.clone();
  }

  public setPoint2(pt2: McGePoint3d)  {
    this.assertWrite();
    this.pt2 = pt2.clone();
  }

  public getPoint1() {
    return this.pt1;
  }

  public getPoint2() {
    return this.pt2;
  }

  // Edit transformation
  public transformBy(_mat: McGeMatrix3d): boolean {
    this.matrix = this.matrix.preMultBy(_mat);
    this.assertWrite();
    return true;
  }

  public getBoundingBox(): {
    minPt: McGePoint3d;
    maxPt: McGePoint3d;
    ret: boolean;
  } {
    return {
      minPt: new McGePoint3d(
        Math.min(this.pt1.x, this.pt2.x),
        Math.min(this.pt1.y, this.pt2.y),
      ),
      maxPt: new McGePoint3d(
        Math.max(this.pt1.x, this.pt2.x),
        Math.max(this.pt1.y, this.pt2.y),
      ),
      ret: true,
    };
  }
}

Register the Entity

ts
import { MxCpp } from "mxcad";

const mxcad = MxCpp.getCurrentMxCAD();
mxcad.on("init", () => {
  new McDbTestLineCustomEntity().rxInit();
});

Setup Method to Draw Target Entity

ts
import { MxCADUiPrPoint, MxCpp } from "mxcad";

async function MxTest_DrawCustomEntity() {
  const getPoint = new MxCADUiPrPoint();
  getPoint.setMessage("\nSpecify first point:");
  let pt1 = await getPoint.go();
  if (!pt1) return;

  getPoint.setBasePt(pt1);
  getPoint.setUseBasePt(true);

  getPoint.setMessage("\nSpecify second point:");
  let pt2 = await getPoint.go();
  if (!pt2) return;

  let myline = new McDbTestLineCustomEntity();
  myline.setPoint1(pt1);
  myline.setPoint2(pt2);
  const mxcad = MxCpp.getCurrentMxCAD();
  mxcad.drawEntity(myline);
}