import { Arc, type Box, Line, Point, type Segment } from '@flatten-js/core'; import { uniqWith } from 'es-toolkit'; import { type Shape, type SnapPoint, SnapPointType, type StartAndEndpointEntity, } from '../App.types'; import type { DrawController } from '../drawControllers/DrawController.ts'; import { getExportColor } from '../helpers/get-export-color'; import { isPointEqual } from '../helpers/is-point-equal'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts'; import { scalePoint } from '../helpers/scale-point'; import { sortPointsOnArc } from '../helpers/sort-points-on-arc'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts'; import { type Entity, EntityName, type JsonEntity } from './Entity'; import type { LineEntity } from './LineEntity.ts'; export class ArcEntity implements Entity, StartAndEndpointEntity { public id: string = crypto.randomUUID(); public lineColor = '#fff'; public lineWidth = 1; public lineDash: number[] | undefined = undefined; public layerId: string; /** 0~1 객체 투명도 (Entity 인터페이스의 선택 항목) */ public opacity?: number; /** GROUP으로 묶인 객체가 공유하는 식별자 */ public groupId?: string; private arc: Arc; public static getAngle(centerPoint: Point, pointOnArc: Point): number { return new Line(centerPoint, pointOnArc).slope; } constructor( layerId: string, centerPoint: Point, radius: number, startAngle: number, endAngle: number, counterClockwise = true ) { this.layerId = layerId; this.arc = new Arc(centerPoint, radius, startAngle, endAngle, counterClockwise); } public draw( drawController: DrawController, parentHighlighted?: boolean, parentSelected?: boolean ): void { drawController.setLineStyles( parentHighlighted ?? isEntityHighlighted(this), parentSelected ?? isEntitySelected(this), this.lineColor, this.lineWidth, this.lineDash ); drawController.drawArc( this.arc.center, this.arc.r.valueOf(), this.arc.startAngle, // 끝각 = 시작각 ± 쓸어간 각 — 끝각 0 을 2π 로 바꾸면 시작각이 음수인 호(대칭 뒤)가 온 원으로 그려짐. this.arc.startAngle + (this.arc.counterClockwise ? this.arc.sweep : -this.arc.sweep), this.arc.counterClockwise ); } public move(x: number, y: number) { this.arc = this.arc.translate(x, y); } public scale(scaleOrigin: Point, scaleFactor: number) { const center = scalePoint(this.arc.center, scaleOrigin, scaleFactor); this.arc = new Arc( center, this.arc.r.valueOf() * scaleFactor, this.arc.startAngle, this.arc.endAngle, this.arc.counterClockwise ); } public rotate(rotateOrigin: Point, angle: number) { this.arc = this.arc.rotate(angle, rotateOrigin); } public mirror(mirrorAxis: LineEntity) { const mirroredCenter = mirrorPointOverAxis(this.arc.center, mirrorAxis); // 축 각 θ 에 대한 대칭 — 각 α → 2θ − α · 도는 방향 반대. const doubleAxisAngle = 2 * mirrorAxis.getAngle(); this.arc = new Arc( mirroredCenter, this.arc.r.valueOf(), doubleAxisAngle - this.arc.startAngle, doubleAxisAngle - this.arc.endAngle, !this.arc.counterClockwise ); } public clone(): Entity { if (this.arc) { const { center, r, startAngle, endAngle, counterClockwise } = this.arc; // 색 · 층 · 선 꼴은 그대로 — 폴리선 자식도 이 길로 복제됨. const copy = new ArcEntity(this.layerId, center, r.valueOf(), startAngle, endAngle, counterClockwise); copy.lineColor = this.lineColor; copy.lineWidth = this.lineWidth; copy.lineDash = this.lineDash; copy.opacity = this.opacity; return copy; } return this; } public intersectsWithBox(box: Box): boolean { return this.arc.intersect(box).length > 0; } public isContainedInBox(box: Box): boolean { return box.contains(this.arc); } public getBoundingBox(): Box { return this.arc.box; } public getShape(): Shape | null { return this.arc; } public getSnapPoints(): SnapPoint[] { return [ { point: this.arc.center, type: SnapPointType.CircleCenter, }, { point: this.arc.start, type: SnapPointType.LineEndPoint, }, { point: this.arc.end, type: SnapPointType.LineEndPoint, }, // TODO add cardinal points if they lay on the arc // TODO add tangent points from mouse location to circle ]; } public getIntersections(entity: Entity): Point[] { const otherShape = entity.getShape(); if (!otherShape) { return []; } return this.arc.intersect(otherShape); } public getFirstPoint(): Point | null { return this.arc.center; } public distanceTo(shape: Shape): [number, Segment] | null { return this.arc.distanceTo(shape); } public getSvgString(): string | null { return ( this.arc.svg({ strokeWidth: this.lineWidth, stroke: getExportColor(this.lineColor), }) || null ); } public getType(): EntityName { return EntityName.Arc; } public containsPointOnShape(point: Point): boolean { if (!this.arc) { return false; } return this.arc.contains(point); } public cutAtPoints(pointsOnShape: Point[]): ArcEntity[] { const points = uniqWith([this.arc.start, this.arc.end, ...pointsOnShape], isPointEqual); const sortedPoints = sortPointsOnArc(points, this.arc.center, this.arc.start); const segmentArcs: ArcEntity[] = []; for (let i = 0; i < sortedPoints.length - 1; i++) { const point1 = sortedPoints[i]; const point2 = sortedPoints[i + 1]; const startAngle = ArcEntity.getAngle(this.arc.center, point1); const endAngle = ArcEntity.getAngle(this.arc.center, point2); const newArc = new ArcEntity( getActiveLayerId(), this.arc.center, Number(this.arc.r), startAngle, endAngle, this.arc.counterClockwise ); newArc.lineColor = this.lineColor; newArc.lineWidth = this.lineWidth; segmentArcs.push(newArc); } return segmentArcs; } public async toJson(): Promise | null> { if (!this.arc) { return null; } return { id: this.id, type: EntityName.Arc, lineColor: this.lineColor, lineWidth: this.lineWidth, lineDash: this.lineDash, layerId: this.layerId, shapeData: { center: { x: this.arc.center.x, y: this.arc.center.y }, radius: this.arc.r.valueOf(), startAngle: this.arc.startAngle, endAngle: this.arc.endAngle, counterClockwise: this.arc.counterClockwise, }, }; } public static async fromJson(jsonEntity: JsonEntity): Promise { if (jsonEntity.type !== EntityName.Arc) { throw new Error('Invalid Entity type in JSON'); } if (!jsonEntity.shapeData) { throw new Error('Invalid JSON entity of type Arc: missing shapeData'); } const center = new Point(jsonEntity.shapeData.center.x, jsonEntity.shapeData.center.y); const radius = jsonEntity.shapeData.radius; const startAngle = jsonEntity.shapeData.startAngle; const endAngle = jsonEntity.shapeData.endAngle; const counterClockwise = jsonEntity.shapeData.counterClockwise; const arcEntity = new ArcEntity( jsonEntity.layerId || getActiveLayerId(), center, radius, startAngle, endAngle, counterClockwise ); arcEntity.id = jsonEntity.id; arcEntity.lineColor = jsonEntity.lineColor; arcEntity.lineWidth = jsonEntity.lineWidth; arcEntity.lineDash = jsonEntity.lineDash; return arcEntity; } public getStartPoint(): Point { return this.arc.start; } public getEndPoint(): Point { return this.arc.end; } } export interface ArcJsonData { center: { x: number; y: number }; radius: number; startAngle: number; endAngle: number; counterClockwise: boolean; }