Preview Drawing
About 5 min
Preview Drawing
Preview drawing provides real-time visual feedback to users and is key to a good CAD command experience. This chapter explains in detail how to implement preview functionality in commands.
Preview Mechanism Overview
WebCAD provides two preview mechanisms:
- Single Entity Preview: Draw a single preview entity (e.g., displaying a circle preview while drawing a circle)
- Multi-Entity Preview: Draw transform preview for multiple entities (e.g., displaying preview positions of selected entities during move/copy)
Single Entity Preview
Using callback
The most common way to preview is through the PointInputOptions.callback callback function:
import { Point2D, CircleEnt, Engine } from 'vjcad';
import { getPoint, PointInputOptions, InputStatusEnum } from 'vjcad';
async function drawCircleWithPreview(): Promise<void> {
// Get center point
const centerOptions = new PointInputOptions("Specify center:");
const centerResult = await getPoint(centerOptions);
if (centerResult.status !== InputStatusEnum.OK) return;
const center = centerResult.value;
// Get radius point (with preview)
const radiusOptions = new PointInputOptions("Specify radius:");
radiusOptions.useBasePoint = true;
radiusOptions.basePoint = center;
// Set preview callback
radiusOptions.callback = (canvasPoint: Point2D) => {
// Convert canvas coordinates to world coordinates
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
// Calculate radius
const radius = center.distanceTo(worldPoint);
// Create preview circle
const previewCircle = new CircleEnt(center, radius);
previewCircle.setDefaults();
// Clear old preview
Engine.pcanvas.drawControl.previewGraphics.clear();
// Draw new preview
Engine.pcanvas.drawControl.drawPreviewEntity(previewCircle);
};
const radiusResult = await getPoint(radiusOptions);
// Clear preview
Engine.pcanvas.drawControl.previewGraphics.clear();
if (radiusResult.status === InputStatusEnum.OK) {
// Create final circle
const radiusPoint = radiusResult.value;
const radius = center.distanceTo(radiusPoint);
const circle = new CircleEnt(center, radius);
circle.setDefaults();
Engine.pcanvas.addEntity(circle);
Engine.undoManager.added_undoMark([circle]);
}
}Preview Drawing API
| Method | Description |
|---|---|
Engine.pcanvas.drawControl.drawPreviewEntity(entity) | Draw a single preview entity |
Engine.pcanvas.drawControl.previewGraphics.clear() | Clear all previews |
Engine.clearPreview() | Clear preview (Engine level) |
Complete Example: Rectangle Preview
import { Point2D, PolylineEnt, BulgePoint, Engine } from 'vjcad';
import {
getPoint,
PointInputOptions,
InputStatusEnum,
ssSetFirst
} from 'vjcad';
export class RectangleCommand {
private corner1: Point2D = new Point2D();
async main(): Promise<void> {
ssSetFirst([]);
Engine.undoManager.start_undoMark();
try {
// Get first corner point
const c1Options = new PointInputOptions("Specify first corner point:");
const c1Result = await getPoint(c1Options);
if (c1Result.status !== InputStatusEnum.OK) return;
this.corner1 = c1Result.value;
// Get opposite corner point (with preview)
const c2Options = new PointInputOptions("Specify opposite corner point:");
c2Options.useBasePoint = true;
c2Options.basePoint = this.corner1;
// Set rectangle preview callback
c2Options.callback = (canvasPoint: Point2D) => {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
this.drawRectanglePreview(worldPoint);
};
const c2Result = await getPoint(c2Options);
// Clear preview
Engine.pcanvas.drawControl.previewGraphics.clear();
if (c2Result.status === InputStatusEnum.OK) {
this.createRectangle(c2Result.value);
}
} finally {
Engine.undoManager.end_undoMark();
}
}
private drawRectanglePreview(corner2: Point2D): void {
const rect = this.buildRectangle(this.corner1, corner2);
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(rect);
}
private buildRectangle(p1: Point2D, p2: Point2D): PolylineEnt {
const pline = new PolylineEnt();
// Four corner points
pline.bulgePoints.add(new BulgePoint(new Point2D(p1.x, p1.y), 0));
pline.bulgePoints.add(new BulgePoint(new Point2D(p2.x, p1.y), 0));
pline.bulgePoints.add(new BulgePoint(new Point2D(p2.x, p2.y), 0));
pline.bulgePoints.add(new BulgePoint(new Point2D(p1.x, p2.y), 0));
pline.isClosed = true;
pline.setDefaults();
return pline;
}
private createRectangle(corner2: Point2D): void {
const rect = this.buildRectangle(this.corner1, corner2);
Engine.pcanvas.addEntity(rect);
Engine.undoManager.added_undoMark([rect]);
}
}Multi-Entity Preview
Commands like move and copy need to preview the transform effect on multiple entities.
Multi-Entity Preview API
| Method | Description |
|---|---|
drawPreviewEntities(entities) | Draw multiple preview entities |
setPreviewPosition(point) | Set preview position |
setPreviewRotation(angle) | Set preview rotation angle |
setPreviewScale(sx, sy) | Set preview scale |
resetPreview() | Reset preview |
Move Command Preview Example
import { Point2D, Engine } from 'vjcad';
import {
getSelections,
getPoint,
SelectionInputOptions,
PointInputOptions,
InputStatusEnum
} from 'vjcad';
export class MoveCommand {
private selectedEntities: any[] = [];
private basePoint: Point2D = new Point2D();
private previewRegistered: boolean = false;
async main(): Promise<void> {
// Select entities
const selResult = await getSelections(new SelectionInputOptions());
if (selResult.status !== InputStatusEnum.OK || selResult.value.length === 0) {
return;
}
this.selectedEntities = selResult.value;
// Highlight display
Engine.pcanvas.highLightEntities(this.selectedEntities);
// Get base point
const baseOptions = new PointInputOptions("Specify base point:");
const baseResult = await getPoint(baseOptions);
if (baseResult.status !== InputStatusEnum.OK) {
Engine.pcanvas.clearHighLight();
return;
}
this.basePoint = baseResult.value;
// Get target point (with multi-entity preview)
const targetOptions = new PointInputOptions("Specify target point:");
targetOptions.useBasePoint = true;
targetOptions.basePoint = this.basePoint;
// Set move preview callback
targetOptions.callback = (canvasPoint: Point2D) => {
this.updateMovePreview(canvasPoint);
};
// Register preview entities (only once)
if (!this.previewRegistered) {
Engine.pcanvas.drawControl.drawPreviewEntities(this.selectedEntities);
this.previewRegistered = true;
}
const targetResult = await getPoint(targetOptions);
// Cleanup
Engine.pcanvas.drawControl.resetPreview();
Engine.pcanvas.clearHighLight();
if (targetResult.status === InputStatusEnum.OK) {
this.executeMove(targetResult.value);
}
}
private updateMovePreview(canvasPoint: Point2D): void {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
// Calculate preview position
const previewPosition = new Point2D();
previewPosition.move(this.basePoint.clone(), new Point2D());
previewPosition.move(new Point2D(), worldPoint);
// Update preview position
Engine.pcanvas.drawControl.setPreviewPosition(previewPosition);
}
private executeMove(targetPoint: Point2D): void {
Engine.undoManager.start_undoMark();
for (const entity of this.selectedEntities) {
entity.move(this.basePoint, targetPoint);
}
Engine.undoManager.moved_undoMark(
this.selectedEntities,
this.basePoint,
targetPoint
);
Engine.undoManager.end_undoMark();
Engine.pcanvas.regen();
}
}Preview with Transforms (Rotation, Mirror)
import { Point2D, Engine } from 'vjcad';
import { ANGLE_90 } from 'vjcad';
class TransformPreviewCommand {
private selectedEntities: any[] = [];
private basePoint: Point2D = new Point2D();
private rotationAngle: number = 0;
private flipX: boolean = false;
private flipY: boolean = false;
private updatePreview(canvasPoint: Point2D): void {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
// Calculate scale factors (for mirroring)
const scaleX = this.flipX ? -1 : 1;
const scaleY = this.flipY ? -1 : 1;
// Calculate transformed position
const scaledBase = new Point2D(
this.basePoint.x * scaleX,
this.basePoint.y * scaleY
);
const transformedPoint = new Point2D();
transformedPoint.move(scaledBase, new Point2D());
transformedPoint.rotate(this.rotationAngle, new Point2D());
transformedPoint.move(new Point2D(), worldPoint);
// Apply preview transforms
Engine.pcanvas.drawControl.setPreviewPosition(transformedPoint);
Engine.pcanvas.drawControl.setPreviewRotation(-this.rotationAngle);
Engine.pcanvas.drawControl.setPreviewScale(scaleX, scaleY);
}
// Rotate left 90 degrees
rotateLeft(): void {
this.rotationAngle += ANGLE_90;
}
// Rotate right 90 degrees
rotateRight(): void {
this.rotationAngle -= ANGLE_90;
}
// Flip horizontally
toggleFlipX(): void {
this.flipX = !this.flipX;
}
// Flip vertically
toggleFlipY(): void {
this.flipY = !this.flipY;
}
}Copy Command Complete Example
import { Point2D, Engine, ANGLE_90 } from 'vjcad';
import {
getSelections,
getPoint,
SelectionInputOptions,
PointInputOptions,
InputStatusEnum,
writeMessage
} from 'vjcad';
import { normalizeAngleAlt } from 'vjcad';
export class CopyCommand {
private selectedEntities: any[] = [];
private previewEntities: any[] = [];
private basePoint: Point2D = new Point2D();
private rotationAngle: number = 0;
private flipX: boolean = false;
private flipY: boolean = false;
private previewRegistered: boolean = false;
async main(): Promise<void> {
// Select entities
const selResult = await getSelections();
if (selResult.status !== InputStatusEnum.OK || selResult.value.length === 0) {
return;
}
this.selectedEntities = selResult.value;
// Clone for preview
this.previewEntities = this.selectedEntities.map(e => e.clone());
// Highlight original entities
Engine.pcanvas.highLightEntities(this.selectedEntities);
// Get base point
const baseOptions = new PointInputOptions("Specify base point:");
const baseResult = await getPoint(baseOptions);
if (baseResult.status !== InputStatusEnum.OK) {
this.cleanup();
return;
}
this.basePoint = baseResult.value;
// Continuous copy loop
await this.copyLoop();
this.cleanup();
}
private async copyLoop(): Promise<void> {
while (true) {
const options = new PointInputOptions(
"Specify copy target [Rotate Left(L)/Rotate Right(R)/Flip Horizontal(X)/Flip Vertical(Y)] <Done>:"
);
options.keywords = ["L", "R", "X", "Y"];
options.useBasePoint = true;
options.basePoint = this.basePoint;
// Set preview callback
options.callback = (canvasPoint: Point2D) => {
this.updatePreview(canvasPoint);
};
// Register preview entities
if (!this.previewRegistered) {
Engine.pcanvas.drawControl.drawPreviewEntities(this.previewEntities);
this.previewRegistered = true;
}
const result = await getPoint(options);
if (result.status === InputStatusEnum.OK) {
// Execute copy
Engine.pcanvas.drawControl.resetPreview();
this.previewRegistered = false;
this.executeCopy(result.value);
} else if (result.status === InputStatusEnum.Keyword) {
this.handleKeyword(result.stringResult);
} else {
// Done or cancel
break;
}
}
}
private updatePreview(canvasPoint: Point2D): void {
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
const scaleX = this.flipX ? -1 : 1;
const scaleY = this.flipY ? -1 : 1;
const scaledBase = new Point2D(
this.basePoint.x * scaleX,
this.basePoint.y * scaleY
);
const transformedPoint = new Point2D();
transformedPoint.move(scaledBase, new Point2D());
transformedPoint.rotate(this.rotationAngle, new Point2D());
transformedPoint.move(new Point2D(), worldPoint);
Engine.pcanvas.drawControl.setPreviewPosition(transformedPoint);
Engine.pcanvas.drawControl.setPreviewRotation(-this.rotationAngle);
Engine.pcanvas.drawControl.setPreviewScale(scaleX, scaleY);
}
private handleKeyword(keyword: string): void {
switch (keyword.toUpperCase()) {
case "L":
this.rotationAngle = normalizeAngleAlt(this.rotationAngle + ANGLE_90);
writeMessage("<br/>Rotated left 90 degrees");
break;
case "R":
this.rotationAngle = normalizeAngleAlt(this.rotationAngle - ANGLE_90);
writeMessage("<br/>Rotated right 90 degrees");
break;
case "X":
this.flipX = !this.flipX;
writeMessage(`<br/>Flip horizontal = ${this.flipX ? "ON" : "OFF"}`);
break;
case "Y":
this.flipY = !this.flipY;
writeMessage(`<br/>Flip vertical = ${this.flipY ? "ON" : "OFF"}`);
break;
}
}
private executeCopy(targetPoint: Point2D): void {
Engine.undoManager.start_undoMark();
const copiedEntities: any[] = [];
const verticalLine = new Point2D(this.basePoint.x, this.basePoint.y + 1);
const horizontalLine = new Point2D(this.basePoint.x + 1, this.basePoint.y);
for (const entity of this.selectedEntities) {
const cloned = entity.clone();
// Apply transforms
if (this.flipX) {
cloned.mirror(this.basePoint, verticalLine);
}
if (this.flipY) {
cloned.mirror(this.basePoint, horizontalLine);
}
if (this.rotationAngle !== 0) {
cloned.rotate(this.basePoint, this.rotationAngle);
}
cloned.move(this.basePoint, targetPoint);
Engine.pcanvas.addEntity(cloned);
copiedEntities.push(cloned);
}
Engine.undoManager.added_undoMark(copiedEntities);
Engine.undoManager.end_undoMark();
Engine.pcanvas.redraw();
}
private cleanup(): void {
Engine.pcanvas.drawControl.resetPreview();
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();
}
}Highlight Display
Highlight display is used to identify selected entities. Unlike preview, highlighting is a visual emphasis on existing entities.
Highlight API
| Method | Description |
|---|---|
Engine.pcanvas.highLightEntities(entities) | Highlight multiple entities |
Engine.pcanvas.drawControl.drawHighLightEntity(entity) | Highlight a single entity |
Engine.pcanvas.clearHighLight() | Clear all highlights |
Highlight Usage Example
// Highlight after selection
const result = await getSelections();
if (result.status === InputStatusEnum.OK && result.value.length > 0) {
Engine.pcanvas.highLightEntities(result.value);
Engine.pcanvas.redraw();
}
// Clear after operation is complete
Engine.pcanvas.clearHighLight();
Engine.pcanvas.regen();Single Entity Highlight (During Cyclic Selection)
while (true) {
const result = await getEntity(options);
if (result.status === InputStatusEnum.OK && result.pickedEntity) {
// Highlight the currently selected entity
Engine.pcanvas.drawControl.drawHighLightEntity(result.pickedEntity);
Engine.pcanvas.redraw();
// Process entity...
// Clear highlight
Engine.pcanvas.clearHighLight();
} else {
break;
}
}Best Practices
1. Always Clean Up Preview
try {
options.callback = (pt) => {
Engine.pcanvas.drawControl.previewGraphics.clear();
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};
const result = await getPoint(options);
} finally {
// Ensure preview is cleaned up
Engine.pcanvas.drawControl.previewGraphics.clear();
// Or
Engine.clearPreview();
}2. Clear Old Preview in Callback
options.callback = (canvasPoint: Point2D) => {
// Clear old preview first
Engine.pcanvas.drawControl.previewGraphics.clear();
// Then draw new preview
const preview = createPreviewEntity(canvasPoint);
Engine.pcanvas.drawControl.drawPreviewEntity(preview);
};3. Coordinate Conversion
The callback function receives canvas coordinates, which need to be converted to world coordinates:
options.callback = (canvasPoint: Point2D) => {
// Canvas coordinates to world coordinates
const worldPoint = Engine.trans.CanvasToWcs(canvasPoint);
// Use world coordinates to create preview
const preview = new CircleEnt(center, center.distanceTo(worldPoint));
// ...
};4. Register Multi-Entity Preview Only Once
// Good practice
if (!this.previewRegistered) {
Engine.pcanvas.drawControl.drawPreviewEntities(entities);
this.previewRegistered = true;
}
// Bad practice (registers on every callback)
options.callback = (pt) => {
Engine.pcanvas.drawControl.drawPreviewEntities(entities); // Duplicate registration!
Engine.pcanvas.drawControl.setPreviewPosition(pt);
};5. Use setDefaults() for Preview Entities
const preview = new CircleEnt(center, radius);
preview.setDefaults(); // Use current layer, color and other defaults
Engine.pcanvas.drawControl.drawPreviewEntity(preview);Next Steps
- Command Design Patterns - Learn different command design patterns
- Best Practices - More development best practices
- API Reference - Complete API reference