import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, subtractV2D } from "../util/geometry"; import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, ROUNTANGLE_RADIUS, TEXT_SNAP_THRESHOLD } from "../App/parameters"; import { VisualEditorState } from "../App/VisualEditor/VisualEditor"; import { sides } from "@/util/geometry"; export type Rountangle = { uid: string; kind: "and" | "or"; } & Rect2D; export type Diamond = { uid: string; } & Rect2D; export type Text = { uid: string; topLeft: Vec2D; text: string; }; export type Arrow = { uid: string; } & Line2D; export type History = { uid: string; kind: "shallow" | "deep"; topLeft: Vec2D; }; export type ConcreteSyntax = { rountangles: Rountangle[]; texts: Text[]; arrows: Arrow[]; diamonds: Diamond[]; history: History[]; }; // independently moveable parts of our shapes: export type RectSide = "left" | "top" | "right" | "bottom"; export type ArrowPart = "start" | "end"; export const emptyState: VisualEditorState = { rountangles: [], texts: [], arrows: [], diamonds: [], history: [], nextID: 0, selection: [], }; // used to find which rountangle an arrow connects to (src/tgt) export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RectSide} | undefined { let best = Infinity; let bestSide: undefined | {uid: string, part: RectSide}; for (const rountangle of candidates) { for (const [side, getSide] of sides) { const asLine = getSide(rountangle); const intersection = intersectLines(arrow, asLine); if (intersection !== null) { const bbox = lineBBox(asLine, ARROW_SNAP_THRESHOLD); const dist = euclideanDistance(arrow[arrowPart], intersection); if (isWithin(arrow[arrowPart], bbox) && dist < best) { best = dist; bestSide = { uid: rountangle.uid, part: side }; } } } } return bestSide; } export function point2LineDistance(point: Vec2D, {start, end}: Line2D): number { const A = point.x - start.x; const B = point.y - start.y; const C = end.x - start.x; const D = end.y - start.y; const dot = A * C + B * D; const lenSq = C * C + D * D; let t = lenSq ? dot / lenSq : -1; if (t < 0) t = 0; else if (t > 1) t = 1; const closestX = start.x + t * C; const closestY = start.y + t * D; const dx = point.x - closestX; const dy = point.y - closestY; const distance = Math.hypot(dx, dy); return distance; } // used to find which arrow a text label belongs to (if any) // author: ChatGPT export function findNearestArrow(point: Vec2D, candidates: Arrow[]): Arrow | undefined { let best; let bestDistance = Infinity for (const arrow of candidates) { const distance = point2LineDistance(point, arrow); if (distance < TEXT_SNAP_THRESHOLD && distance < bestDistance) { bestDistance = distance; best = arrow; } } return best; } // precondition: candidates are sorted from big to small export function findRountangle(point: Vec2D, candidates: Rountangle[]): Rountangle | undefined { for (let i=candidates.length-1; i>=0; i--) { if (isWithin(point, candidates[i])) { return candidates[i]; } } } export function findNearestHistory(point: Vec2D, candidates: History[]): History | undefined { let best; let bestDistance = Infinity; for (const h of candidates) { const diff = subtractV2D(point, {x: h.topLeft.x+HISTORY_RADIUS, y: h.topLeft.y+HISTORY_RADIUS}); const euclideanDistance = Math.hypot(diff.x, diff.y) - HISTORY_RADIUS; if (euclideanDistance < ARROW_SNAP_THRESHOLD) { if (euclideanDistance < bestDistance) { best = h; bestDistance = euclideanDistance; } } } return best; } export function rountangleMinSize(size: Vec2D): Vec2D { const minSize = ROUNTANGLE_RADIUS * 2; if (size.x >= minSize && size.y >= minSize) { return size; } return { x: Math.max(minSize, size.x), y: Math.max(minSize, size.y), }; }