Files
Aislo/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts
eomsangdonandClaude Opus 5 65d6fd261f fix(B07): 도각 편집 띠 사이드바 이관·깨진 그림에 렌더 루프 정지 해소
- 편집 띠를 CAD 위 절대배치에서 도면 목록 하단 액션 칸으로 옮김 —
  1행 편집 띠, 2행 [도각 편집]·[현재 도면 확정] (리본과 겹쳐 문구·버튼이 찌그러지던 문제)
- `drawImage` 가 'broken' 상태 그림에서 던지던 예외로 렌더 루프가 끊겨
  이후 모든 도면이 백지로 남던 결함 해소 — 못 읽은 그림은 건너뜀
- `ImageEntity` 가 자리표시자(`{{회사로고}}`)를 절대 URL 로 굳히던 저장 경로 수정 —
  원본 주소를 보관해 그대로 돌려주고, 자리표시자는 요청조차 하지 않음
- 버튼 줌(ZOOMIN·ZOOMOUT)이 배율만 바꿔 도면이 화면 밖으로 밀리던 것을
  화면 중심 고정으로 수정

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:12:28 +09:00

683 lines
22 KiB
TypeScript

import { Point, type Vector } from '@flatten-js/core';
import { WHEEL_ZOOM_EXPONENT } from '../App.consts';
import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts';
import { getBoundingBoxOfMultipleEntities } from '../helpers/get-bounding-box-of-multiple-entities.ts';
import { mapNumberRange } from '../helpers/map-number-range.ts';
import { StateVariable } from '../helpers/undo-stack.ts';
import { getEntities, getGridEnabled, getPickableEntities, triggerReactUpdate } from '../state.ts';
import { paintColor, themeColor } from '../theme.ts';
import { DEFAULT_TEXT_OPTIONS, type DrawController } from './DrawController';
/** 토큰을 못 읽을 때만 쓰는 캔버스 배경 폴백. */
const CANVAS_BACKGROUND_FALLBACK = '#111';
/**
* Screen coordinate system:
* 0, 0 X
* +---------->
* |
* |
* |
* Y v
*
*
* World coordinate system:
* Y ^
* |
* |
* |
* +---------->
* 0, 0 X
*
* To convert between the 2 coordinate systems, you need the screenOffset and screenScale
*/
// Batch-mode stroke decimation: skip chain segments shorter than this (screen px)
const BATCH_LOD_PX = 0.5;
// Batch-mode text smaller than this (screen px) is unreadable — skip drawing it
const BATCH_MIN_TEXT_PX = 2;
export class ScreenCanvasDrawController implements DrawController {
private screenOffset: Point = new Point(0, 0);
private screenScale = 1;
private screenMouseLocation: Point;
private canvasSize: Point = new Point(100, 100);
// Style-run batching (static scene rendering): consecutive stroke calls
// with the same style are collected into one Path2D and stroked once.
private batching = false;
private batchPath: Path2D | null = null;
private batchKey: string | null = null;
private batchStyle: { color: string; lineWidth: number; dash: number[] } | null = null;
private batchLastX = Number.NaN;
private batchLastY = Number.NaN;
constructor(private context: CanvasRenderingContext2D) {
this.screenMouseLocation = new Point(this.canvasSize.x / 2, this.canvasSize.y / 2);
this.setScreenOffset(new Point(0, 0)); // User expects mathematical coordinates, where y axis goes up, but canvas y axis goes down
}
public getCanvasSize() {
return this.canvasSize;
}
public setCanvasSize(newCanvasSize: Point) {
this.canvasSize = newCanvasSize;
}
public getScreenScale() {
return this.screenScale;
}
public setScreenScale(newScreenScale: number) {
this.screenScale = newScreenScale;
triggerReactUpdate(StateVariable.screenZoom);
}
public getScreenOffset() {
return this.screenOffset;
}
public setScreenOffset(newScreenOffset: Point) {
this.screenOffset = newScreenOffset;
triggerReactUpdate(StateVariable.screenOffset);
}
public setScreenMouseLocation(newScreenMouseLocation: Point): void {
this.screenMouseLocation = newScreenMouseLocation;
triggerReactUpdate(StateVariable.screenMouseLocation);
}
public getWorldMouseLocation(): Point {
return this.targetToWorld(this.screenMouseLocation);
}
public getScreenMouseLocation(): Point {
return this.screenMouseLocation;
}
public panScreen(screenOffsetX: number, screenOffsetY: number) {
this.screenOffset = new Point(
this.screenOffset.x - screenOffsetX / this.screenScale,
this.screenOffset.y - screenOffsetY / this.screenScale
);
}
/**
* This function takes the deltaY from the mouse wheel event and zooms the screen in or out
* The location of the mouse in world space is preserved
* @param deltaY
* @param exponent 델타 한 단위당 확대 지수 (휠·핀치가 서로 다른 값을 쓴다)
*/
public zoomScreen(deltaY: number, exponent: number = WHEEL_ZOOM_EXPONENT) {
const worldMouseLocationBeforeZoom = this.getWorldMouseLocation();
const oldScreenScale = this.getScreenScale();
// 부호가 아니라 크기까지 본다 — 트랙패드의 잔 델타는 그만큼만 움직인다.
const newScreenScale = oldScreenScale * Math.exp(-deltaY * exponent);
this.setScreenScale(newScreenScale);
// now get the location of the cursor in world space again
// It will have changed because the scale has changed,
// but we can offset our world now to fix the zoom location in screen space,
// because we know how much it changed laterally between the two spatial scales.
const worldMouseLocationAfterZoom = this.getWorldMouseLocation();
const offsetAdjustment = new Point(
worldMouseLocationBeforeZoom.x - worldMouseLocationAfterZoom.x,
worldMouseLocationBeforeZoom.y - worldMouseLocationAfterZoom.y
);
// Adjust the screen offset to maintain the cursor position
this.screenOffset = new Point(
this.screenOffset.x + offsetAdjustment.x,
this.screenOffset.y + offsetAdjustment.y
);
}
/**
* 전체 도면(모든 엔티티)을 화면 중심에 여백을 두고 배치한다.
* 가로/세로 중 더 제약이 큰 축에 맞춰 배율을 정하고, 도면 중심이 화면 중심에
* 오도록 screenOffset(월드 좌표)을 역산한다. (기존 구현은 화면 픽셀 여백값을
* 월드 좌표 offset에 그대로 대입해 중심 배치가 어긋나는 문제가 있었다.)
*/
public zoomToFitScreen() {
// 도각·원지반 같은 잠금 도면층은 빼고 설계 콘텐츠에만 맞춘다 (2026-08-31 사용자
// 확정). 도각까지 넣으면 A1 한 장 전체가 잡혀 화면의 40%가 여백으로 간다.
const pickable = getPickableEntities();
const entities = pickable.length ? pickable : getEntities();
if (!entities.length) return;
const boundingBox = getBoundingBoxOfMultipleEntities(entities);
const boundingWidth = boundingBox.maxX - boundingBox.minX;
const boundingHeight = boundingBox.maxY - boundingBox.minY;
const canvasSize = this.getCanvasSize();
// 10% 여백을 남기고 두 축 중 더 빡빡한 쪽에 맞춘다 (종횡비 유지)
const FIT_MARGIN = 0.9;
const scaleX =
boundingWidth > 0 ? (canvasSize.x * FIT_MARGIN) / boundingWidth : Number.POSITIVE_INFINITY;
const scaleY =
boundingHeight > 0 ? (canvasSize.y * FIT_MARGIN) / boundingHeight : Number.POSITIVE_INFINITY;
let zoomLevel = Math.min(scaleX, scaleY);
if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) zoomLevel = 1;
this.setScreenScale(zoomLevel);
// screen = (world - offset) * zoom 이므로, 도면 중심을 화면 중심에 맞추려면
// offset = worldCenter - (화면 절반 픽셀) / zoom
const worldCenterX = (boundingBox.minX + boundingBox.maxX) / 2;
const worldCenterY = (boundingBox.minY + boundingBox.maxY) / 2;
this.setScreenOffset(
new Point(
worldCenterX - canvasSize.x / 2 / zoomLevel,
worldCenterY - canvasSize.y / 2 / zoomLevel
)
);
}
/**
* Convert coordinates from World Space --> Screen Space
*/
public worldToTarget(worldCoordinate: Point): Point {
return new Point(
mapNumberRange(
worldCoordinate.x,
this.screenOffset.x,
this.screenOffset.x + this.canvasSize.x / this.screenScale,
0,
this.canvasSize.x
),
mapNumberRange(
worldCoordinate.y,
this.screenOffset.y,
this.screenOffset.y + this.canvasSize.y / this.screenScale,
0,
this.canvasSize.y
)
);
}
public worldsToTargets(worldCoordinates: Point[]): Point[] {
return worldCoordinates.map(this.worldToTarget.bind(this));
}
/**
* Convert coordinates from Screen Space --> World Space
* (0, 0) (1920, 0)
*
* (0, 1080) (1920, 1080)
*
* convert to
*
* (0, 1080) (1920, 1080)
*
* (0, 0) (1920, 0)
*/
public targetToWorld(screenCoordinate: Point): Point {
// map the screen coordinate to the world coordinate based on this.getScreenOffset() and the this.getScreenScale()
return new Point(
mapNumberRange(
screenCoordinate.x,
0,
this.canvasSize.x,
this.screenOffset.x,
this.screenOffset.x + this.canvasSize.x / this.screenScale
),
mapNumberRange(
screenCoordinate.y,
0,
this.canvasSize.y,
this.screenOffset.y,
this.screenOffset.y + this.canvasSize.y / this.screenScale
)
);
}
public targetsToWorlds(screenCoordinates: Point[]): Point[] {
return screenCoordinates.map(this.targetToWorld.bind(this));
}
public setLineStyles(
isHighlighted: boolean,
isSelected: boolean,
rawColor: string,
lineWidth: number,
dash: number[] = []
) {
// 저장된 도면 색은 다크 배경 기준 — 라이트 테마에서는 그릴 때만 대비를 맞춘다.
const color = paintColor(rawColor);
if (this.batching) {
const effectiveWidth = isHighlighted ? lineWidth + 1 : lineWidth;
const effectiveDash = isSelected ? [5, 5] : dash;
const key = `${color}|${effectiveWidth}|${effectiveDash.join(',')}`;
if (key !== this.batchKey) {
this.flushBatch();
this.batchKey = key;
this.batchStyle = { color, lineWidth: effectiveWidth, dash: effectiveDash };
}
return;
}
this.context.strokeStyle = color;
this.context.lineWidth = lineWidth;
this.context.setLineDash(dash);
if (isHighlighted) {
this.context.lineWidth = lineWidth + 1;
}
if (isSelected) {
this.context.setLineDash([5, 5]);
}
}
/**
* Start style-run batching: consecutive stroke calls sharing a style are
* accumulated into a single Path2D and stroked once (with sub-pixel
* segment decimation). Used while rendering the static scene cache.
*/
public beginBatch() {
this.flushBatch();
this.batching = true;
this.batchKey = null;
this.batchStyle = null;
}
public endBatch() {
this.flushBatch();
this.batching = false;
this.batchKey = null;
this.batchStyle = null;
}
private flushBatch() {
if (this.batchPath && this.batchStyle) {
this.context.strokeStyle = this.batchStyle.color;
this.context.lineWidth = this.batchStyle.lineWidth;
this.context.setLineDash(this.batchStyle.dash);
// Round caps/joins replace the per-segment endpoint dots drawn in
// the unbatched path (see _drawRoundedEndpoint)
this.context.lineCap = 'round';
this.context.lineJoin = 'round';
this.context.stroke(this.batchPath);
this.context.lineCap = 'butt';
this.context.lineJoin = 'miter';
}
this.batchPath = null;
this.batchLastX = Number.NaN;
this.batchLastY = Number.NaN;
}
/** 도면층·객체 투명도 (1이면 불투명) */
public setOpacity(opacity: number) {
this.context.globalAlpha = Math.min(1, Math.max(0, opacity));
}
public setFillStyles(fillColor: string) {
this.context.fillStyle = paintColor(fillColor);
}
/**
* Temporarily redirect all draw calls to another 2d context (eg an
* offscreen canvas used as static scene cache), reusing the current
* offset/scale/canvasSize without touching state or react triggers.
*/
public withContext(temporaryContext: CanvasRenderingContext2D, renderFunction: () => void) {
const originalContext = this.context;
this.context = temporaryContext;
try {
renderFunction();
} finally {
this.context = originalContext;
}
}
/**
* Blit a pre-rendered bitmap (static scene cache) onto the canvas at a
* pixel offset. Used while panning to avoid re-stroking every entity.
*/
public blitImage(source: CanvasImageSource, dx: number, dy: number) {
this.context.drawImage(source, dx, dy);
}
public clear() {
if (this.canvasSize === null) return;
if (!this.context) return;
if (this.batching) this.flushBatch();
this.context.fillStyle = themeColor('--cad-canvas', CANVAS_BACKGROUND_FALLBACK);
this.context.fillRect(0, 0, this.canvasSize?.x, this.canvasSize?.y);
if (getGridEnabled()) {
this.context.strokeStyle = themeColor('--color-mist', '#242b35');
this.context.lineWidth = 1;
this.context.setLineDash([]);
this.context.beginPath();
for (let x = 0.5; x < this.canvasSize.x; x += 24) {
this.context.moveTo(x, 0);
this.context.lineTo(x, this.canvasSize.y);
}
for (let y = 0.5; y < this.canvasSize.y; y += 24) {
this.context.moveTo(0, y);
this.context.lineTo(this.canvasSize.x, y);
}
this.context.stroke();
}
}
/**
* Draws a line from startPoint to endPoint and auto converts to screen space first
* @param worldStartPoint
* @param worldEndPoint
*/
public drawLine(worldStartPoint: Point, worldEndPoint: Point): void {
const [screenStartPoint, screenEndPoint] = this.worldsToTargets([
worldStartPoint,
worldEndPoint,
]);
this.drawLineScreen(screenStartPoint, screenEndPoint);
}
/**
* Needs to be public to draw UI that is zoom independent, like snap point indicators
* @param screenStartPoint
* @param screenEndPoint
*/
public drawLineScreen(screenStartPoint: Point, screenEndPoint: Point): void {
if (this.batching) {
const startX = screenStartPoint.x;
const startY = this.canvasSize.y - screenStartPoint.y;
const endX = screenEndPoint.x;
const endY = this.canvasSize.y - screenEndPoint.y;
if (!this.batchPath) this.batchPath = new Path2D();
// Chain break: start is not where the previous segment ended
const chainBroken =
Math.abs(startX - this.batchLastX) > BATCH_LOD_PX ||
Math.abs(startY - this.batchLastY) > BATCH_LOD_PX;
if (chainBroken || Number.isNaN(this.batchLastX)) {
this.batchPath.moveTo(startX, startY);
this.batchLastX = startX;
this.batchLastY = startY;
}
const isTinyStep =
Math.abs(endX - this.batchLastX) < BATCH_LOD_PX &&
Math.abs(endY - this.batchLastY) < BATCH_LOD_PX;
// Decimate sub-pixel steps inside a chain; isolated segments always draw
if (!isTinyStep || chainBroken) {
this.batchPath.lineTo(endX, endY);
this.batchLastX = endX;
this.batchLastY = endY;
}
return;
}
this.context.beginPath();
this.context.moveTo(screenStartPoint.x, this.canvasSize.y - screenStartPoint.y);
this.context.lineTo(screenEndPoint.x, this.canvasSize.y - screenEndPoint.y);
this.context.stroke();
const lineWidth = this.context.lineWidth;
const style = this.context.strokeStyle as string;
this._drawRoundedEndpoint(screenStartPoint, lineWidth, style);
this._drawRoundedEndpoint(screenEndPoint, lineWidth, style);
}
private _drawRoundedEndpoint(screenPoint: Point, lineWidth: number, style: string): void {
this.context.fillStyle = style;
this.context.beginPath();
this.context.arc(
screenPoint.x,
this.canvasSize.y - screenPoint.y,
lineWidth / 2,
0,
2 * Math.PI
);
this.context.fill();
}
/**
* Draw an arc (segment of a circle) or a circle if startAngle = 0 and endAngle = 2PI
* @param centerPoint
* @param radius
* @param startAngle
* @param endAngle
* @param counterClockWise
*/
public drawArc(
centerPoint: Point,
radius: number,
startAngle: number,
endAngle: number,
counterClockWise: boolean
) {
const screenCenterPoint = this.worldToTarget(centerPoint);
const screenRadius = radius * this.screenScale;
// Flip angles over the x-axis, because we go from world to screen coordinates which flips the y-axis direction
this.drawArcScreen(screenCenterPoint, screenRadius, -startAngle, -endAngle, counterClockWise);
}
public drawArcScreen(
screenCenterPoint: Point,
screenRadius: number,
startAngle: number,
endAngle: number,
counterClockWise: boolean
) {
if (this.batching) {
if (screenRadius < BATCH_LOD_PX) return; // invisible at this zoom
if (!this.batchPath) this.batchPath = new Path2D();
const centerX = screenCenterPoint.x;
const centerY = this.canvasSize.y - screenCenterPoint.y;
this.batchPath.moveTo(
centerX + screenRadius * Math.cos(startAngle),
centerY + screenRadius * Math.sin(startAngle)
);
this.batchPath.arc(centerX, centerY, screenRadius, startAngle, endAngle, counterClockWise);
// Arc end becomes the new chain tail
this.batchLastX = centerX + screenRadius * Math.cos(endAngle);
this.batchLastY = centerY + screenRadius * Math.sin(endAngle);
return;
}
this.context.beginPath();
this.context.arc(
screenCenterPoint.x,
this.canvasSize.y - screenCenterPoint.y,
screenRadius,
startAngle,
endAngle,
counterClockWise
);
this.context.stroke();
const lineWidth = this.context.lineWidth;
const style = this.context.strokeStyle as string;
// Calculate arc endpoints
const startScreenX = screenCenterPoint.x + screenRadius * Math.cos(startAngle);
// Y is inverted in canvas, but also for the arc angles, so we subtract from canvasSize.y and then add sin
const startScreenY =
this.canvasSize.y - screenCenterPoint.y + screenRadius * Math.sin(startAngle);
const endScreenX = screenCenterPoint.x + screenRadius * Math.cos(endAngle);
const endScreenY = this.canvasSize.y - screenCenterPoint.y + screenRadius * Math.sin(endAngle);
// Convert back to Point objects, note that _drawRoundedEndpoint expects y to be from top of canvas
const arcStartPoint = new Point(startScreenX, this.canvasSize.y - startScreenY);
const arcEndPoint = new Point(endScreenX, this.canvasSize.y - endScreenY);
this._drawRoundedEndpoint(arcStartPoint, lineWidth, style);
this._drawRoundedEndpoint(arcEndPoint, lineWidth, style);
}
/**
* Draw some text at the base location
* The direction vector points from the bottom of the first letter towards the bottom of the last letter indicate the rotation of the text * @param label
* @param label
* @param basePoint
* @param options
*/
public drawText(
label: string,
basePoint: Point,
options: Partial<{
textDirection?: Vector;
textAlign: 'left' | 'center' | 'right';
textColor: string;
fontSize: number;
fontFamily: string;
bold: boolean;
italic: boolean;
}> = {}
): void {
const screenBasePoint = this.worldToTarget(basePoint);
this.drawTextScreen(label, screenBasePoint, {
...options,
fontSize: options.fontSize ? options.fontSize * this.screenScale : undefined,
});
}
/**
* Draw some text at the base location
* The direction vector points from the bottom of the first letter towards the bottom of the last letter indicate the rotation of the text
* @param label
* @param basePoint
* @param options
*/
public drawTextScreen(
label: string,
basePoint: Point,
options: Partial<{
textDirection?: Vector;
textAlign: 'left' | 'center' | 'right';
textColor: string;
fontSize: number;
fontFamily: string;
bold: boolean;
italic: boolean;
}> = {}
): void {
const opts = {
...DEFAULT_TEXT_OPTIONS,
...options,
};
if (this.batching) {
if (opts.fontSize < BATCH_MIN_TEXT_PX) return; // unreadable at this zoom
this.flushBatch(); // keep draw order: strokes so far go under this text
}
this.context.save();
this.context.translate(basePoint.x, this.canvasSize.y - basePoint.y);
const angle = getAngleWithXAxis(
new Point(0, 0),
new Point(opts.textDirection.x, -opts.textDirection.y)
);
this.context.rotate(angle);
this.context.font = `${opts.italic ? 'italic ' : ''}${opts.bold ? 'bold ' : ''}${opts.fontSize}px ${opts.fontFamily}`;
this.context.textAlign = opts.textAlign;
this.context.fillStyle = paintColor(opts.textColor);
this.context.textBaseline = 'middle';
this.context.fillText(label, 0, 0);
this.context.restore();
}
/**
* Draw an image to the canvas using world coordinates
* @param imageElement
* @param xMin
* @param yMin
* @param width
* @param height
* @param angle
*/
public drawImage(
imageElement: HTMLImageElement,
xMin: number,
yMin: number,
width: number,
height: number,
angle: number
): void {
if (this.batching) this.flushBatch();
// 못 읽은 그림은 건너뛴다. 'broken' 상태의 그림을 그리려 하면 캔버스가 예외를
// 던지고, 그 예외가 렌더 루프를 끊어 **이후 모든 도면이 백지**로 남았다
// (2026-09-02 실측: 도각 자리표시자가 404 로 깨진 채 줌 한 번에 화면이 멈췄다).
// 자리 하나가 비는 것과 도면 전체가 안 나오는 것은 무게가 다르다.
if (imageElement.complete === false || imageElement.naturalWidth === 0) return;
// 크기는 배율만 곱한다. 예전에는 (width, height)를 좌표처럼 변환해 화면 오프셋과
// y 뒤집기가 섞여 들어갔고, 그림이 제 자리를 벗어나 비율까지 무너졌다(2026-09-02
// 도각 로고·서명에서 드러남). 자리는 SVG 컨트롤러와 같이 **세계 중심**으로 잡는다.
const screenWidth = width * this.screenScale;
const screenHeight = height * this.screenScale;
const screenCenter = this.worldToTarget(new Point(xMin + width / 2, yMin + height / 2));
const screenCenterX = screenCenter.x;
// 다른 그리기와 같이 y 를 뒤집는다 — worldToTarget 은 수학 좌표(위가 +y)라 그대로
// 쓰면 그림이 거울상 자리(캔버스 밖)에 놓여 화면에 안 보인다(2026-09-02 도각 로고).
const screenCenterY = this.canvasSize.y - screenCenter.y;
// Rotate and translate context (y 를 뒤집었으니 회전 방향도 반대)
this.context.translate(screenCenterX, screenCenterY);
this.context.rotate(-angle);
// Draw image
this.context.drawImage(
imageElement,
-screenWidth / 2,
-screenHeight / 2,
screenWidth,
screenHeight
);
// Reset context
this.context.rotate(angle);
this.context.translate(-screenCenterX, -screenCenterY);
}
public fillRect(xMin: number, yMin: number, width: number, height: number, color: string) {
const screenMinPoint = this.worldToTarget(new Point(xMin, yMin));
this.fillRectScreen(
screenMinPoint.x,
screenMinPoint.y,
width * this.screenScale,
height * this.screenScale,
color
);
}
/**
* Fill rectangle with color, but interpret the provided coordinates as screen coordinates
* @param xMin
* @param yMin
* @param width
* @param height
* @param color
*/
public fillRectScreen(xMin: number, yMin: number, width: number, height: number, color: string) {
if (this.batching) this.flushBatch();
// TODO see if we need to replace this with a call to fillPolygon
this.context.fillStyle = paintColor(color);
// 화면 좌표는 아래에서 위로 재는 규약이다(getCanvasPoint·drawLineScreen과 같다).
// yMin은 사각형의 아래 변이므로 캔버스 y로 옮길 때 높이를 함께 빼야 제자리다.
// 안 빼서 선택 그립이 선보다 8px 아래에 찍혔고, 줌을 해도 화면 픽셀 어긋남이라
// 간격이 그대로였다(2026-08-30 사용자 지적).
this.context.fillRect(xMin, this.canvasSize.y - yMin - height, width, height);
}
/**
* Fill polygon with color
* @param points
*/
public fillPolygon(...points: Point[]) {
if (this.batching) this.flushBatch();
const screenPoints = points.map(this.worldToTarget.bind(this));
this.context.beginPath();
screenPoints.forEach((screenPoint, index) => {
if (index === 0) {
this.context.moveTo(screenPoint.x, this.canvasSize.y - screenPoint.y);
} else {
this.context.lineTo(screenPoint.x, this.canvasSize.y - screenPoint.y);
}
});
this.context.closePath();
this.context.fill();
}
}