Custom Entities (CustomEntityBase)
About 3 min
Custom Entities (CustomEntityBase)
CustomEntityBase is the base class for creating custom entity types, allowing developers to extend WebCAD's entity system.
Overview
When standard entities cannot meet your needs, you can create custom entities by inheriting from CustomEntityBase to implement specific geometric shapes, interaction behaviors, and display effects.
Creating Custom Entities
Basic Structure
import {
CustomEntityBase,
Point2D,
BoundingBox,
Engine
} from 'vjcad';
class CrossMarkEntity extends CustomEntityBase {
private _center: Point2D;
private _size: number;
constructor(center: Point2D = new Point2D(0, 0), size: number = 10) {
super();
this.type = 'CROSS_MARK'; // Custom type identifier
this._center = center;
this._size = size;
}
// Center point
get center(): Point2D {
return this._center;
}
set center(val: Point2D) {
this._center = val;
this.setModified();
}
// Size
get size(): number {
return this._size;
}
set size(val: number) {
this._size = val;
this.setModified();
}
// Calculate bounding box
boundingBox(): BoundingBox {
const half = this._size / 2;
return new BoundingBox(
this._center.x - half,
this._center.y - half,
this._center.x + half,
this._center.y + half
);
}
// Clone
clone(): CrossMarkEntity {
const cloned = new CrossMarkEntity(
new Point2D(this._center.x, this._center.y),
this._size
);
this.clonePropertiesTo(cloned);
return cloned;
}
// Move
move(from: Point2D, to: Point2D): void {
const dx = to.x - from.x;
const dy = to.y - from.y;
this._center = new Point2D(
this._center.x + dx,
this._center.y + dy
);
this.setModified();
}
// Rotate (rotation is meaningless for a cross mark, keep unchanged)
rotate(base: Point2D, angle: number): void {
// Only move the center point
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const dx = this._center.x - base.x;
const dy = this._center.y - base.y;
this._center = new Point2D(
base.x + dx * cos - dy * sin,
base.y + dx * sin + dy * cos
);
this.setModified();
}
// Scale
scale(base: Point2D, factor: number): void {
this._center = new Point2D(
base.x + (this._center.x - base.x) * factor,
base.y + (this._center.y - base.y) * factor
);
this._size *= Math.abs(factor);
this.setModified();
}
// Serialize (using Point2D.toDb() to auto-handle 2D/3D format)
toDb(): object {
return {
...super.toDb(),
center: this._center.toDb(), // Outputs [x, y] or [x, y, z]
size: this._size
};
}
// Deserialize
fromDb(data: any): void {
super.fromDb(data);
this._center = new Point2D().fromDb(data.center); // Supports array and object format
this._size = data.size;
}
}Register Custom Entity
import { EntityRegistry } from 'vjcad';
// Register custom entity type
EntityRegistry.register('CROSS_MARK', CrossMarkEntity);
// Usage
const mark = new CrossMarkEntity(new Point2D(100, 100), 20);
mark.setDefaults();
Engine.addEntities(mark);Implementing Rendering
Custom entities need to provide rendering logic:
import { CustomEntityBase, Point2D, LineEnt, CircleEnt } from 'vjcad';
class CrossMarkEntity extends CustomEntityBase {
// ... property definitions ...
// Get sub-entities for rendering
getDisplayEntities(): EntityBase[] {
const half = this._size / 2;
const center = this._center;
// Horizontal line
const hLine = new LineEnt(
new Point2D(center.x - half, center.y),
new Point2D(center.x + half, center.y)
);
hLine.color = this.color;
// Vertical line
const vLine = new LineEnt(
new Point2D(center.x, center.y - half),
new Point2D(center.x, center.y + half)
);
vLine.color = this.color;
// Center circle
const circle = new CircleEnt(center, half * 0.3);
circle.color = this.color;
return [hLine, vLine, circle];
}
}Implementing Grip Editing
class CrossMarkEntity extends CustomEntityBase {
// ... other code ...
// Get grip point positions
getGripPoints(): Point2D[] {
const half = this._size / 2;
return [
this._center, // Center point
new Point2D(this._center.x + half, this._center.y), // Right
new Point2D(this._center.x, this._center.y + half), // Top
];
}
// Grip editing
gripEdit(gripIndex: number, from: Point2D, to: Point2D): void {
const dx = to.x - from.x;
const dy = to.y - from.y;
if (gripIndex === 0) {
// Move center point
this._center = new Point2D(
this._center.x + dx,
this._center.y + dy
);
} else if (gripIndex === 1) {
// Adjust size (right grip)
this._size = Math.abs(to.x - this._center.x) * 2;
} else if (gripIndex === 2) {
// Adjust size (top grip)
this._size = Math.abs(to.y - this._center.y) * 2;
}
this.setModified();
}
}Complete Example: Labeled Marker Point
import {
CustomEntityBase,
Point2D,
BoundingBox,
LineEnt,
CircleEnt,
TextEnt,
Engine
} from 'vjcad';
class LabeledMarker extends CustomEntityBase {
private _position: Point2D;
private _label: string;
private _markerSize: number;
private _textHeight: number;
constructor(
position: Point2D = new Point2D(0, 0),
label: string = '',
markerSize: number = 10,
textHeight: number = 5
) {
super();
this.type = 'LABELED_MARKER';
this._position = position;
this._label = label;
this._markerSize = markerSize;
this._textHeight = textHeight;
}
get position(): Point2D { return this._position; }
set position(val: Point2D) { this._position = val; this.setModified(); }
get label(): string { return this._label; }
set label(val: string) { this._label = val; this.setModified(); }
get markerSize(): number { return this._markerSize; }
set markerSize(val: number) { this._markerSize = val; this.setModified(); }
get textHeight(): number { return this._textHeight; }
set textHeight(val: number) { this._textHeight = val; this.setModified(); }
boundingBox(): BoundingBox {
const half = this._markerSize / 2;
const textWidth = this._label.length * this._textHeight * 0.7;
return new BoundingBox(
this._position.x - half,
this._position.y - half,
this._position.x + Math.max(half, textWidth),
this._position.y + this._markerSize + this._textHeight
);
}
getDisplayEntities(): EntityBase[] {
const entities: EntityBase[] = [];
const pos = this._position;
const half = this._markerSize / 2;
// Cross lines
const hLine = new LineEnt(
new Point2D(pos.x - half, pos.y),
new Point2D(pos.x + half, pos.y)
);
hLine.color = this.color;
entities.push(hLine);
const vLine = new LineEnt(
new Point2D(pos.x, pos.y - half),
new Point2D(pos.x, pos.y + half)
);
vLine.color = this.color;
entities.push(vLine);
// Circle
const circle = new CircleEnt(pos, half);
circle.color = this.color;
entities.push(circle);
// Label text
if (this._label) {
const text = new TextEnt();
text.position = new Point2D(pos.x, pos.y + this._markerSize);
text.text = this._label;
text.height = this._textHeight;
text.color = this.color;
entities.push(text);
}
return entities;
}
clone(): LabeledMarker {
const cloned = new LabeledMarker(
new Point2D(this._position.x, this._position.y),
this._label,
this._markerSize,
this._textHeight
);
this.clonePropertiesTo(cloned);
return cloned;
}
move(from: Point2D, to: Point2D): void {
const dx = to.x - from.x;
const dy = to.y - from.y;
this._position = new Point2D(
this._position.x + dx,
this._position.y + dy
);
this.setModified();
}
toDb(): object {
return {
...super.toDb(),
position: this._position.toDb(), // Outputs [x, y] or [x, y, z]
label: this._label,
markerSize: this._markerSize,
textHeight: this._textHeight
};
}
fromDb(data: any): void {
super.fromDb(data);
this._position = new Point2D().fromDb(data.position); // Supports array and object format
this._label = data.label;
this._markerSize = data.markerSize;
this._textHeight = data.textHeight;
}
}
// Use the custom entity
const marker = new LabeledMarker(
new Point2D(100, 100),
'P1',
15,
6
);
marker.color = 1; // Red
marker.setDefaults();
Engine.addEntities(marker);Best Practices
- Type identifier: Use a unique
typestring to identify custom entities - Serialization: Properly implement
toDb()andfromDb()to support save and load - Clone: Implement
clone()to support copy operations - Bounding box: Accurately calculate bounding boxes to support selection and zoom-to-fit
- Grip points: Provide intuitive grip point locations for editing