Files
Aislo/B07_DesignDetail/openwebcad/src/tools/eraser-tool.helpers.ts
T
eomsangdonandClaude Opus 5.5 00a379595b feat(CAD): 337-4a 다-1 고치기 7 — OFFSET · FILLET · CHAMFER · TRIM · EXTEND · BREAK · JOIN 오토캐드 물음 차례 · 옵션 · 기본값 기억 · 빈 Enter · Esc · 끝난 뒤
- OFFSET 새 파일(offset-tool.ts) — 거리 <통과점> · 통과점(T) · 지우기(E) 예/아니오 · 객체 → 면의 점 되풀이 · 다중(M) <다음 객체> · 명령 취소(U) · 기억 OFFSETDIST · OFFSETERASE
- FILLET · CHAMFER 한 꼴 — 객체 먼저 · 반지름(R) · 거리(D) 둘(둘째 기본 = 첫째) · 자르기(T) 모드 · 다중(M) · 명령 취소(U) · Shift = 구석 · 기억 FILLETRAD · CHAMFERA/B · TRIMMODE
- TRIM · EXTEND 빠른 모드 한 꼴 — 되풀이 · Shift = 반대 일 · 집기 반경 밖 무시 · 지우기(R) · 숨은 U · 사각형 변 원래 층 · 꼴(N9) · 자른 선 꼴 유지
- BREAK — 누른 점 = 첫 끊기점 · 첫 번째 점(F) · @0,0 = 같은 점 · JOIN 물음 글
- 시험 resources/tester/cad/modify-acad.test.ts 13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-16 00:14:29 +09:00

182 lines
6.3 KiB
TypeScript

import { type Circle, Point, Segment } from '@flatten-js/core';
import { compact } from 'es-toolkit';
import { ArcEntity } from '../entities/ArcEntity';
import type { CircleEntity } from '../entities/CircleEntity';
import type { Entity } from '../entities/Entity';
import { LineEntity } from '../entities/LineEntity';
import { findClosestEntity } from '../helpers/find-closest-entity';
import { findNeighboringPointsOnArc } from '../helpers/find-neighboring-points-on-arc';
import { findNeighboringPointsOnCircle } from '../helpers/find-neighboring-points-on-circle';
import { findNeighboringPointsOnLine } from '../helpers/find-neighboring-points-on-line';
import { sampleEntityPoints } from '../helpers/geometry/sample-entity';
import { intersectLines } from '../helpers/geometry/shape-points';
import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts';
import { isPointEqual } from '../helpers/is-point-equal';
import { pickRadius } from '../helpers/pick-radius';
import { queryEntitiesNearPoint } from '../helpers/spatial-index';
import { addEntities, deleteEntities, getActiveLayerId, getLayerById } from '../state';
import { copyStyle, isBetween } from './modify/modify.helpers';
/** 누른 자리 집기 반경 안 가장 가까운 객체 — 잠금 도면층 뺌(순서 엔진과 같은 셈) */
export function pickEntityAt(point: Point): Entity | null {
const radius = pickRadius();
const candidates = queryEntitiesNearPoint(point.x, point.y, radius).filter(
(entity) => !getLayerById(entity.layerId)?.isLocked
);
const { distance, entity } = findClosestEntity(point, candidates);
return entity && distance <= radius ? entity : null;
}
/** 점을 객체 위로 내림(가장 가까운 자리) */
export const closestPointOn = (entity: Entity, point: Point): Point =>
findClosestEntity(point, [entity]).segment?.start ?? point;
/** EXTEND 빠른 모드 — 누른 쪽 끝을 그 방향 가장 가까운 경계(다른 모든 객체)까지 · 선만 */
export function extendLineEnd(line: Entity, pick: Point, boundaries: Entity[]): Entity | null {
const shape = line.getShape();
if (!(shape instanceof Segment)) return null;
const fromStart = pick.distanceTo(shape.start)[0] < pick.distanceTo(shape.end)[0];
const tip = fromStart ? shape.start : shape.end;
const tail = fromStart ? shape.end : shape.start;
const length = tail.distanceTo(tip)[0];
if (!length) return null;
const dx = (tip.x - tail.x) / length;
const dy = (tip.y - tail.y) / length;
let best = Number.POSITIVE_INFINITY;
let hit: Point | null = null;
for (const boundary of boundaries) {
if (boundary.id === line.id) continue;
const points = sampleEntityPoints(boundary);
for (let index = 1; index < points.length; index++) {
const crossing = intersectLines(tail, tip, points[index - 1], points[index]);
if (!crossing || !isBetween(crossing, points[index - 1], points[index])) continue;
const ahead = (crossing.x - tip.x) * dx + (crossing.y - tip.y) * dy;
if (ahead > 1e-9 && ahead < best) {
best = ahead;
hit = crossing;
}
}
}
if (!hit) return null;
return copyStyle(
line,
new LineEntity(line.layerId, fromStart ? hit : tail, fromStart ? tail : hit)
);
}
export function getAllIntersectionPoints(entity: Entity, entities: Entity[]): Point[] {
// TODO see if we need to make this list unique
return compact(
entities
.filter((otherEntity) => otherEntity.id !== entity.id)
.flatMap((otherEntity) => {
if (entity.id === otherEntity.id) {
return null;
}
return entity.getIntersections(otherEntity);
})
);
}
export function eraseLineSegment(
line: LineEntity,
clickedPointOnShape: Point,
intersections: Point[]
): void {
const segment = line.getShape() as Segment;
const [firstCutPoint, secondCutPoint] = findNeighboringPointsOnLine(
clickedPointOnShape,
segment.start,
segment.end,
intersections
);
const cutLines: Entity[] = line.cutAtPoints([firstCutPoint, secondCutPoint]);
// Remove the segment that has the clickedPointOnShape point on it — 남는 토막은 원래 층 · 꼴(N9)
const remainingLines = cutLines
.filter((piece) => !piece.containsPointOnShape(clickedPointOnShape))
.map((piece) => copyStyle(line, piece));
deleteEntities([line], false);
// Helper functions should never trigger an undo state, since they can be called multiple times during one user operation
addEntities(remainingLines, false);
}
export function eraseCircleSegment(
circle: CircleEntity,
clickedPointOnShape: Point,
intersections: Point[]
): void {
if (intersections.length === 0) {
deleteEntities([circle], false);
return;
}
const [firstCutPoint, secondCutPoint] = findNeighboringPointsOnCircle(
clickedPointOnShape,
circle,
intersections
);
const circleShape = circle.getShape() as Circle;
const center = circleShape.center;
const angles = [firstCutPoint, secondCutPoint, clickedPointOnShape].map((p) =>
getAngleWithXAxis(new Point(center.x, center.y), new Point(p.x, p.y))
);
const [startAngle, endAngle] = isAngleBetween(angles[2], angles[0], angles[1])
? [angles[1], angles[0]]
: [angles[0], angles[1]];
const newArc = new ArcEntity(
getActiveLayerId(),
center,
circleShape.r,
startAngle,
endAngle,
true
);
Object.assign(newArc, {
lineColor: circle.lineColor,
lineWidth: circle.lineWidth,
});
deleteEntities([circle], false);
// Helper functions should never trigger an undo state, since they can be called multiple times during one user operation
addEntities([newArc], false);
}
function isAngleBetween(angle: number, start: number, end: number): boolean {
const twoPi = 2 * Math.PI;
return (angle - start + twoPi) % twoPi <= (end - start + twoPi) % twoPi;
}
export function eraseArcSegment(
arc: ArcEntity,
clickedPointOnShape: Point,
intersections: Point[]
): void {
const [first, second] = findNeighboringPointsOnArc(clickedPointOnShape, arc, intersections);
if (isPointEqual(first, second)) {
deleteEntities([arc], true);
return;
}
const newArcs = arc
.cutAtPoints([first, second])
.filter((cutArc) => !cutArc.containsPointOnShape(clickedPointOnShape));
for (const newArc of newArcs) {
Object.assign(newArc, {
lineColor: arc.lineColor,
lineWidth: arc.lineWidth,
});
addEntities([newArc], false);
}
// Helper functions should never trigger an undo state, since they can be called multiple times during one user operation
deleteEntities([arc], false);
}