UNPKG

@webwriter/geometry-cloze

Version:

Create and view geometry exercises with coloring, styling and labeling options.

214 lines (190 loc) 7.02 kB
import Calc, { MathPoint } from '../helper/Calc'; import Vector from '../helper/Vector'; import Arrays from '../helper/Arrays'; import { NamedElement } from './base/Element'; import { StylableData } from './base/Stylable'; import Draggable, { DraggableData } from './base/Draggable'; import Shape from './Shape'; import { ContextMenuItem, ContextMenuSubmenu } from '../../types/ContextMenu'; import Numbers from '../helper/Numbers'; import Manager from '../CanvasManager/Abstracts'; export type BasePoint = MathPoint & NamedElement; export default class Point extends Draggable { protected _x: number; protected _y: number; constructor( manager: Manager, data: BasePoint & Partial<StylableData & DraggableData> & { showOutsideAngle?: boolean } ) { super(manager, data); if (data.showOutsideAngle) this.showOutsideAngle = data.showOutsideAngle; this._x = data.x; this._y = data.y; } protected showOutsideAngle = false; draw(ctx: CanvasRenderingContext2D) { if (this.hidden) return; super.draw(ctx); ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); if (this.showLabel) { ctx.font = '18px Arial'; ctx.fillStyle = this.labelColor; ctx.strokeStyle = this.labelColor; const label = this.getLabel(); let angle = this.getAngle(); const neighbors = this.getNeighborPoints(); const metrics = ctx.measureText(label); const fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; if (angle !== -1 && neighbors) { const textPadding = 2.5; let vec1 = Vector.normalize(Vector.subtract(neighbors[0], this)); let vec2 = Vector.normalize(Vector.subtract(neighbors[1], this)); if (this.showOutsideAngle) angle = 360 - angle; if (angle === 90 && this.manager.abstractRightAngle) { vec1 = Vector.scale(vec1, this.size * 2 + textPadding); vec2 = Vector.scale(vec2, this.size * 2 + textPadding); ctx.moveTo(this.x + vec1.x, this.y + vec2.x); ctx.lineTo(this.x + vec1.x + vec2.x, this.y + vec1.y + vec2.y); ctx.lineTo(this.x + vec2.x, this.y + vec2.y); ctx.stroke(); } else { // get vector inwards of polygon let middle = Vector.normalize(Vector.add(vec1, vec2)); if (Vector.len(middle) === 0) middle = Vector.orthogonal(vec1); if ( Calc.isPointInPolygon( Vector.add(this, middle), (this.parent as Shape).getPoints() ) === this.showOutsideAngle ) middle = Vector.multiply(middle, -1); middle = Vector.normalize(middle, this.size + textPadding); const textDiagonal = Math.sqrt(metrics.width ** 2 + fontHeight ** 2); let radius: number, textX: number, textY: number; // outer label if (angle < 45) { textX = (middle.x + (middle.x > 0 ? 0 : -1) * metrics.width) * -1; textY = (middle.y + (middle.y > 0 ? 1 : 0) * fontHeight) * -1; radius = 20; } else { // inner label textX = middle.x + (middle.x > 0 ? 0 : -1) * metrics.width; textY = middle.y + (middle.y > 0 ? 1 : 0) * fontHeight; radius = textPadding + textDiagonal + 5; } ctx.fillText(label, this.x + textX, this.y + textY); ctx.beginPath(); ctx.arc( this.x, this.y, this.size + radius, Vector.angle({ x: 1, y: 0 }, this.showOutsideAngle ? vec1 : vec2), Vector.angle({ x: 1, y: 0 }, this.showOutsideAngle ? vec2 : vec1) ); ctx.stroke(); } } else { // fallback to simple label ctx.clearRect( this.x + 10, this.y - 30, metrics.width + 10, fontHeight + 10 ); ctx.fillText(label, this.x + 10, this.y - 10); } } } getHit(point: MathPoint, point2?: MathPoint): Draggable[] { if (this.hidden) return []; if (!point2) return Calc.distance(this, point) - this.clickTargetSize < this.size + this.lineWidth / 2 ? [this] : super.getHit(point, point2); else { const isInSelection = Calc.isPointInRect( { x1: point.x, y1: point.y, x2: point2.x, y2: point2.y }, this ); if (isInSelection) return [this]; return super.getHit(point, point2); } } private getNeighborPoints(): [MathPoint, MathPoint] | null { if (!this.parent || !('getLines' in this.parent)) return null; const lines = this.parent.getLines(); const neighbors = lines.filter((l) => l.isEndpoint(this)); if (neighbors.length !== 2) return null; // lines are ordered wrong for first point if (lines[0] === neighbors[0] && Arrays.at(lines, -1) === neighbors[1]) neighbors.reverse(); const points = neighbors.map((l) => (l.start === this ? l.end : l.start)); return points as [MathPoint, MathPoint]; } private getAngle(): number { const neighbors = this.getNeighborPoints(); if (!neighbors) return -1; const vector1 = Vector.subtract(this, neighbors[0]); const vector2 = Vector.subtract(this, neighbors[1]); const angle = Vector.angle(vector1, vector2); const degAngle = (angle * 180) / Math.PI; const rightWay = ((degAngle - 360) * -1) % 360; const rounded = Math.round(rightWay * 100) / 100; return rounded; } protected getValueLabel(): string { let angle = this.getAngle(); if (angle === -1) return ''; if (this.showOutsideAngle) angle = 360 - angle; return `${Numbers.round(angle)}°`; } public getContextMenuItems(): ContextMenuItem[] { const res = [ ...super.getContextMenuItems(), ...this.getStyleContextMenuItems({ stroke: true, fill: true, lineWidth: true, nameList: 'greek' }) ]; ( res.find((i) => i.type === 'submenu' && i.key === 'label') as | ContextMenuSubmenu | undefined )?.items.splice(1, 0, { key: 'showOutsideAngle', type: 'checkbox', label: 'Switch angle', getChecked: () => this.showOutsideAngle, action: (value: boolean) => { this.showOutsideAngle = value; this.requestRedraw(); } }); return res; } public export() { return { ...super.export(), _type: 'point' as const, x: this.x, y: this.y, ...(this.showOutsideAngle && { showOutsideAngle: this.showOutsideAngle }) }; } public static import(data: BasePoint, manager: Manager) { return new Point(manager, data); } }