import { RountanglePart } from "../statecharts/concrete_syntax"; export type Vec2D = { x: number; y: number; }; export type Rect2D = { topLeft: Vec2D; size: Vec2D; }; export type Line2D = { start: Vec2D; end: Vec2D; }; // make sure size is always positive export function normalizeRect(rect: Rect2D) { return { topLeft: { x: rect.size.x < 0 ? (rect.topLeft.x + rect.size.x) : rect.topLeft.x, y: rect.size.y < 0 ? (rect.topLeft.y + rect.size.y) : rect.topLeft.y, }, size: { x: rect.size.x < 0 ? -rect.size.x : rect.size.x, y: rect.size.y < 0 ? -rect.size.y : rect.size.y, } }; } export function isEntirelyWithin(child: Rect2D, parent: Rect2D) { return ( child.topLeft.x >= parent.topLeft.x && child.topLeft.y >= parent.topLeft.y && child.topLeft.x + child.size.x <= parent.topLeft.x + parent.size.x && child.topLeft.y + child.size.y <= parent.topLeft.y + parent.size.y ); } export function isWithin(p: Vec2D, r: Rect2D) { return ( p.x >= r.topLeft.x && p.x <= r.topLeft.x + r.size.x && p.y >= r.topLeft.y && p.y <= r.topLeft.y + r.size.y ); } export function addV2D(a: Vec2D, b: Vec2D) { return { x: a.x + b.x, y: a.y + b.y, }; } export function subtractV2D(a: Vec2D, b: Vec2D) { return { x: a.x - b.x, y: a.y - b.y, }; } export function scaleV2D(p: Vec2D, scale: number) { return { x: p.x * scale, y: p.y * scale, }; } export function area(rect: Rect2D) { return rect.size.x * rect.size.y; } export function lineBBox(line: Line2D, margin=0): Rect2D { return { topLeft: { x: line.start.x - margin, y: line.start.y - margin, }, size: { x: line.end.x - line.start.x + margin*2, y: line.end.y - line.start.y + margin*2, }, } } export function transformRect(rect: Rect2D, parts: string[], delta: Vec2D): Rect2D { return { topLeft: { x: parts.includes("left") ? rect.topLeft.x + delta.x : rect.topLeft.x, y: parts.includes("top") ? rect.topLeft.y + delta.y : rect.topLeft.y, }, size: { x: /*Math.max(40,*/ rect.size.x + (parts.includes("right") ? delta.x : 0) - (parts.includes("left") ? delta.x : 0), y: /*Math.max(40,*/ rect.size.y + (parts.includes("bottom") ? delta.y : 0) - (parts.includes("top") ? delta.y : 0), }, }; } export function transformLine(line: Line2D, parts: string[], delta: Vec2D): Line2D { return { start: parts.includes("start") ? addV2D(line.start, {x: delta.x, y: delta.y}) : line.start, end: parts.includes("end") ? addV2D(line.end, {x: delta.x, y: delta.y}) : line.end, }; } // intersection point of two lines // note: point may not be part of the lines // author: ChatGPT export function intersectLines(a: Line2D, b: Line2D): Vec2D | null { const { start: A1, end: A2 } = a; const { start: B1, end: B2 } = b; const den = (A1.x - A2.x) * (B1.y - B2.y) - (A1.y - A2.y) * (B1.x - B2.x); if (den === 0) return null; // parallel or coincident const x = ((A1.x * A2.y - A1.y * A2.x) * (B1.x - B2.x) - (A1.x - A2.x) * (B1.x * B2.y - B1.y * B2.x)) / den; const y = ((A1.x * A2.y - A1.y * A2.x) * (B1.y - B2.y) - (A1.y - A2.y) * (B1.x * B2.y - B1.y * B2.x)) / den; return { x, y }; } export function euclideanDistance(a: Vec2D, b: Vec2D): number { const diffX = a.x - b.x; const diffY = a.y - b.y; return Math.hypot(diffX, diffY); // return Math.sqrt(diffX*diffX + diffY*diffY); } export function getLeftSide(rect: Rect2D): Line2D { return { start: rect.topLeft, end: {x: rect.topLeft.x, y: rect.topLeft.y + rect.size.y}, }; } export function getTopSide(rect: Rect2D): Line2D { return { start: rect.topLeft, end: { x: rect.topLeft.x + rect.size.x, y: rect.topLeft.y }, }; } export function getRightSide(rect: Rect2D): Line2D { return { start: { x: rect.topLeft.x + rect.size.x, y: rect.topLeft.y }, end: { x: rect.topLeft.x + rect.size.x, y: rect.topLeft.y + rect.size.y }, }; } export function getBottomSide(rect: Rect2D): Line2D { return { start: { x: rect.topLeft.x, y: rect.topLeft.y + rect.size.y }, end: { x: rect.topLeft.x + rect.size.x, y: rect.topLeft.y + rect.size.y }, }; } export type ArcDirection = "no" | "cw" | "ccw"; export function arcDirection(start: RountanglePart, end: RountanglePart): ArcDirection { if (start === end) { if (start === "left" || start === "top") { return "ccw"; } else { return "cw"; } } const both = [start, end]; if (both.includes("top") && both.includes("bottom")) { return "no"; } if (both.includes("left") && both.includes("right")) { return "no"; } if (start === "top" && end === "left") { return "ccw"; } if (start === "left" && end === "bottom") { return "ccw"; } if (start === "bottom" && end === "right") { return "ccw"; } if (start === "right" && end === "top") { return "ccw"; } return "cw"; }