scriptable-testlab
Version:
A lightweight, efficient tool designed to manage and update scripts for Scriptable.
434 lines (389 loc) • 9.96 kB
text/typescript
import {AbsDrawContext} from 'scriptable-abstract';
import {MockImage} from '../media/image';
import {MockSize} from './size';
interface DrawOperation {
type: 'image' | 'path' | 'rect' | 'ellipse' | 'text';
data: any;
}
interface DrawContextState {
size: Size;
respectScreenScale: boolean;
opaque: boolean;
currentPath?: Path;
fillColor?: Color;
strokeColor?: Color;
lineWidth: number;
canvas: MockImage;
font?: Font;
textColor?: Color;
textAlignment: 'left' | 'center' | 'right';
operations: DrawOperation[];
}
const DEFAULT_STATE: DrawContextState = {
size: new MockSize(0, 0) as unknown as Size,
respectScreenScale: true,
opaque: false,
lineWidth: 1,
canvas: new MockImage() as unknown as MockImage,
textAlignment: 'left',
operations: [],
};
/**
* Mock implementation of Scriptable's DrawContext.
* Provides a context for drawing shapes, text and images.
* @implements DrawContext
*/
export class MockDrawContext extends AbsDrawContext<DrawContextState> {
constructor() {
super(DEFAULT_STATE);
}
/**
* Creates a new drawing context with the specified size.
*/
static create(width: number, height: number): DrawContext {
const context = new MockDrawContext();
context.size = new MockSize(width, height) as unknown as Size;
return context;
}
get size(): Size {
return this.state.size;
}
set size(value: Size) {
this.setState({size: value});
}
get respectScreenScale(): boolean {
return this.state.respectScreenScale;
}
set respectScreenScale(value: boolean) {
this.setState({respectScreenScale: value});
}
get opaque(): boolean {
return this.state.opaque;
}
set opaque(value: boolean) {
this.setState({opaque: value});
}
/**
* Gets the image that was drawn in the context.
*/
getImage(): Image {
return this.state.canvas;
}
/**
* Draws an image in the specified rect.
*/
drawImageInRect(image: Image, rect: Rect): void {
this.state.operations.push({
type: 'image',
data: {image, rect, mode: 'rect'},
});
this.setState({
canvas: image as MockImage,
operations: this.state.operations,
});
}
/**
* Draws an image at the specified point.
*/
drawImageAtPoint(image: Image, point: Point): void {
this.state.operations.push({
type: 'image',
data: {image, point, mode: 'point'},
});
this.setState({
canvas: image as MockImage,
operations: this.state.operations,
});
}
/**
* Sets the fill color used when filling paths and shapes.
*/
setFillColor(color: Color): void {
this.setState({fillColor: color});
}
/**
* Sets the stroke color used when stroking paths and shapes.
*/
setStrokeColor(color: Color): void {
this.setState({strokeColor: color});
}
/**
* Sets the width of lines when stroking paths and shapes.
*/
setLineWidth(width: number): void {
if (width < 0) {
throw new Error('Line width must be non-negative');
}
this.setState({lineWidth: width});
}
/**
* Begins a new path.
*/
beginPath(): void {
// In mock implementation, we don't create actual paths
this.setState({currentPath: undefined});
}
/**
* Moves to a point in the current path.
*/
moveToPoint(point: Point): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {type: 'moveTo', point},
});
this.setState({operations: this.state.operations});
}
/**
* Adds a line to a point in the current path.
*/
addLineToPoint(point: Point): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {type: 'lineTo', point},
});
this.setState({operations: this.state.operations});
}
/**
* Adds a curve to the current path.
*/
addCurveToPoint(point: Point, control1: Point, control2: Point): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {type: 'curveTo', point, control1, control2},
});
this.setState({operations: this.state.operations});
}
/**
* Adds a quadratic curve to the current path.
*/
addQuadCurveToPoint(point: Point, control: Point): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {type: 'quadCurveTo', point, control},
});
this.setState({operations: this.state.operations});
}
/**
* Closes the current path.
*/
closePath(): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {type: 'close'},
});
this.setState({operations: this.state.operations});
}
/**
* Strokes the current path.
*/
strokePath(): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {
type: 'stroke',
color: this.state.strokeColor,
lineWidth: this.state.lineWidth,
},
});
this.setState({operations: this.state.operations});
}
/**
* Fills the current path.
*/
fillPath(): void {
// Store operation for tracking
this.state.operations.push({
type: 'path',
data: {
type: 'fill',
color: this.state.fillColor,
},
});
this.setState({operations: this.state.operations});
}
/**
* Fills and strokes the current path.
*/
fillAndStrokePath(): void {
this.fillPath();
this.strokePath();
}
/**
* Adds a rectangle to the current path.
*/
addRect(rect: Rect): void {
const {x, y, width, height} = rect;
this.moveToPoint({x, y});
this.addLineToPoint({x: x + width, y});
this.addLineToPoint({x: x + width, y: y + height});
this.addLineToPoint({x, y: y + height});
this.closePath();
}
/**
* Adds an ellipse to the current path.
*/
addEllipseInRect(rect: Rect): void {
// Approximate ellipse with Bezier curves
const {x, y, width, height} = rect;
const kappa = 0.5522848;
const ox = (width / 2) * kappa;
const oy = (height / 2) * kappa;
const xe = x + width;
const ye = y + height;
const xm = x + width / 2;
const ym = y + height / 2;
this.moveToPoint({x, y: ym});
this.addCurveToPoint({x: xm, y}, {x, y: y + oy}, {x: xm - ox, y});
this.addCurveToPoint({x: xe, y: ym}, {x: xm + ox, y}, {x: xe, y: ym - oy});
this.addCurveToPoint({x: xm, y: ye}, {x: xe, y: ym + oy}, {x: xm + ox, y: ye});
this.addCurveToPoint({x, y: ym}, {x: xm - ox, y: ye}, {x, y: ym + oy});
}
/**
* Fills a rectangle.
*/
fill(rect: Rect): void {
this.fillRect(rect);
}
/**
* Fills a rectangle.
*/
fillRect(rect: Rect): void {
this.state.operations.push({
type: 'rect',
data: {rect, mode: 'fill', color: this.state.fillColor},
});
this.setState({operations: this.state.operations});
}
/**
* Fills an ellipse.
*/
fillEllipse(rect: Rect): void {
this.state.operations.push({
type: 'ellipse',
data: {rect, mode: 'fill', color: this.state.fillColor},
});
this.setState({operations: this.state.operations});
}
/**
* Strokes a rectangle.
*/
stroke(rect: Rect): void {
this.strokeRect(rect);
}
/**
* Strokes a rectangle.
*/
strokeRect(rect: Rect): void {
this.state.operations.push({
type: 'rect',
data: {
rect,
mode: 'stroke',
color: this.state.strokeColor,
lineWidth: this.state.lineWidth,
},
});
this.setState({operations: this.state.operations});
}
/**
* Strokes an ellipse.
*/
strokeEllipse(rect: Rect): void {
this.state.operations.push({
type: 'ellipse',
data: {
rect,
mode: 'stroke',
color: this.state.strokeColor,
lineWidth: this.state.lineWidth,
},
});
this.setState({operations: this.state.operations});
}
/**
* Adds a path to the context.
*/
addPath(path: Path): void {
this.setState({currentPath: path});
}
/**
* Draws text at a position.
*/
drawText(text: string, pos: Point): void {
if (!this.state.textColor) {
throw new Error('Text color must be set before drawing text');
}
if (!this.state.font) {
throw new Error('Font must be set before drawing text');
}
this.state.operations.push({
type: 'text',
data: {
text,
pos,
color: this.state.textColor,
font: this.state.font,
alignment: this.state.textAlignment,
},
});
this.setState({operations: this.state.operations});
}
/**
* Draws text in a rectangle.
*/
drawTextInRect(text: string, rect: Rect): void {
if (!this.state.textColor) {
throw new Error('Text color must be set before drawing text');
}
if (!this.state.font) {
throw new Error('Font must be set before drawing text');
}
this.state.operations.push({
type: 'text',
data: {
text,
rect,
color: this.state.textColor,
font: this.state.font,
alignment: this.state.textAlignment,
},
});
this.setState({operations: this.state.operations});
}
/**
* Sets the font to use when drawing text.
*/
setFont(font: Font): void {
this.setState({font});
}
/**
* Sets the text color used when drawing text.
*/
setTextColor(color: Color): void {
this.setState({textColor: color});
}
/**
* Specifies that texts should be left aligned.
*/
setTextAlignedLeft(): void {
this.setState({textAlignment: 'left'});
}
/**
* Specifies that texts should be center aligned.
*/
setTextAlignedCenter(): void {
this.setState({textAlignment: 'center'});
}
/**
* Specifies that texts should be right aligned.
*/
setTextAlignedRight(): void {
this.setState({textAlignment: 'right'});
}
}