JS/JSX 77개 파일을 TS/TSX로 전환하고 JS 원본을 삭제. - stores 7개, map core 6개, hooks 4개 등 전체 모듈 전환 - TypeScript strict 모드, OL/Deck.gl 타입 적용 - .gitignore에서 TS/TSX 무시 규칙 제거 - pre-commit hook: .js,.jsx → .ts,.tsx 확장자 변경 - tsc --noEmit 0 에러, ESLint 0 에러, yarn build 성공 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
/**
|
|
* 사각형(Box) 리사이즈 커스텀 인터랙션
|
|
*
|
|
* OL Modify는 사각형 제약을 지원하지 않으므로 PointerInteraction을 확장.
|
|
* - 모서리 드래그: 대각 꼭짓점 고정, 자유 리사이즈
|
|
* - 변 드래그: 반대쪽 변 고정, 1축 리사이즈
|
|
*/
|
|
import PointerInteraction from 'ol/interaction/Pointer';
|
|
import type Feature from 'ol/Feature';
|
|
import type { Polygon } from 'ol/geom';
|
|
import type MapBrowserEvent from 'ol/MapBrowserEvent';
|
|
import type OlMap from 'ol/Map';
|
|
import type { Coordinate } from 'ol/coordinate';
|
|
|
|
const CORNER_TOLERANCE = 16;
|
|
const EDGE_TOLERANCE = 12;
|
|
|
|
/** 점(p)과 선분(a-b) 사이 최소 픽셀 거리 */
|
|
function pointToSegmentDist(p: number[], a: number[], b: number[]): number {
|
|
const dx = b[0] - a[0];
|
|
const dy = b[1] - a[1];
|
|
const lenSq = dx * dx + dy * dy;
|
|
if (lenSq === 0) return Math.hypot(p[0] - a[0], p[1] - a[1]);
|
|
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq;
|
|
t = Math.max(0, Math.min(1, t));
|
|
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
}
|
|
|
|
interface BoxResizeInteractionOptions {
|
|
feature: Feature<Polygon>;
|
|
onResize?: (feature: Feature<Polygon>) => void;
|
|
}
|
|
|
|
interface HandleResult {
|
|
cursor: string;
|
|
}
|
|
|
|
interface BBox {
|
|
minX: number;
|
|
maxX: number;
|
|
minY: number;
|
|
maxY: number;
|
|
}
|
|
|
|
export default class BoxResizeInteraction extends PointerInteraction {
|
|
private feature_: Feature<Polygon>;
|
|
private onResize_: ((feature: Feature<Polygon>) => void) | null;
|
|
// corner mode
|
|
private mode_: 'corner' | 'edge' | null;
|
|
private anchorCoord_: Coordinate | null;
|
|
// edge mode
|
|
private edgeIndex_: number | null;
|
|
private bbox_: BBox | null;
|
|
|
|
constructor(options: BoxResizeInteractionOptions) {
|
|
super({
|
|
handleDownEvent: (evt: MapBrowserEvent<PointerEvent>) => BoxResizeInteraction.prototype._handleDown.call(this, evt),
|
|
handleDragEvent: (evt: MapBrowserEvent<PointerEvent>) => BoxResizeInteraction.prototype._handleDrag.call(this, evt),
|
|
handleUpEvent: () => BoxResizeInteraction.prototype._handleUp.call(this),
|
|
});
|
|
this.feature_ = options.feature;
|
|
this.onResize_ = options.onResize || null;
|
|
// corner mode
|
|
this.mode_ = null; // 'corner' | 'edge'
|
|
this.anchorCoord_ = null;
|
|
// edge mode
|
|
this.edgeIndex_ = null;
|
|
this.bbox_ = null;
|
|
}
|
|
|
|
private _handleDown(evt: MapBrowserEvent<PointerEvent>): boolean {
|
|
const pixel = evt.pixel as unknown as number[];
|
|
const coords = this.feature_.getGeometry()!.getCoordinates()[0];
|
|
|
|
// 1. 모서리 감지 (우선)
|
|
for (let i = 0; i < 4; i++) {
|
|
const vp = evt.map.getPixelFromCoordinate(coords[i]);
|
|
if (Math.hypot(pixel[0] - vp[0], pixel[1] - vp[1]) < CORNER_TOLERANCE) {
|
|
this.mode_ = 'corner';
|
|
this.anchorCoord_ = coords[(i + 2) % 4];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 2. 변 감지
|
|
for (let i = 0; i < 4; i++) {
|
|
const j = (i + 1) % 4;
|
|
const p1 = evt.map.getPixelFromCoordinate(coords[i]);
|
|
const p2 = evt.map.getPixelFromCoordinate(coords[j]);
|
|
if (pointToSegmentDist(pixel, p1 as unknown as number[], p2 as unknown as number[]) < EDGE_TOLERANCE) {
|
|
this.mode_ = 'edge';
|
|
this.edgeIndex_ = i;
|
|
const xs = coords.slice(0, 4).map((c: Coordinate) => c[0]);
|
|
const ys = coords.slice(0, 4).map((c: Coordinate) => c[1]);
|
|
this.bbox_ = {
|
|
minX: Math.min(...xs), maxX: Math.max(...xs),
|
|
minY: Math.min(...ys), maxY: Math.max(...ys),
|
|
};
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private _handleDrag(evt: MapBrowserEvent<PointerEvent>): void {
|
|
const coord = evt.coordinate;
|
|
|
|
if (this.mode_ === 'corner') {
|
|
const anchor = this.anchorCoord_!;
|
|
const minX = Math.min(coord[0], anchor[0]);
|
|
const maxX = Math.max(coord[0], anchor[0]);
|
|
const minY = Math.min(coord[1], anchor[1]);
|
|
const maxY = Math.max(coord[1], anchor[1]);
|
|
this.feature_.getGeometry()!.setCoordinates([[
|
|
[minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY], [minX, maxY],
|
|
]]);
|
|
} else if (this.mode_ === 'edge') {
|
|
let { minX, maxX, minY, maxY } = this.bbox_!;
|
|
// Edge 0: top(TL->TR), 1: right(TR->BR), 2: bottom(BR->BL), 3: left(BL->TL)
|
|
switch (this.edgeIndex_) {
|
|
case 0: maxY = coord[1]; break;
|
|
case 1: maxX = coord[0]; break;
|
|
case 2: minY = coord[1]; break;
|
|
case 3: minX = coord[0]; break;
|
|
}
|
|
const x1 = Math.min(minX, maxX), x2 = Math.max(minX, maxX);
|
|
const y1 = Math.min(minY, maxY), y2 = Math.max(minY, maxY);
|
|
this.feature_.getGeometry()!.setCoordinates([[
|
|
[x1, y2], [x2, y2], [x2, y1], [x1, y1], [x1, y2],
|
|
]]);
|
|
}
|
|
}
|
|
|
|
private _handleUp(): boolean {
|
|
if (this.mode_) {
|
|
this.mode_ = null;
|
|
this.anchorCoord_ = null;
|
|
this.edgeIndex_ = null;
|
|
this.bbox_ = null;
|
|
if (this.onResize_) this.onResize_(this.feature_);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 호버 감지: 픽셀이 리사이즈 핸들 위인지 확인
|
|
*/
|
|
isOverHandle(map: OlMap, pixel: number[]): HandleResult | null {
|
|
const coords = this.feature_.getGeometry()!.getCoordinates()[0];
|
|
|
|
// 모서리 감지
|
|
const cornerCursors = ['nwse-resize', 'nesw-resize', 'nwse-resize', 'nesw-resize'];
|
|
for (let i = 0; i < 4; i++) {
|
|
const vp = map.getPixelFromCoordinate(coords[i]);
|
|
if (Math.hypot(pixel[0] - vp[0], pixel[1] - vp[1]) < CORNER_TOLERANCE) {
|
|
return { cursor: cornerCursors[i] };
|
|
}
|
|
}
|
|
|
|
// 변 감지
|
|
const edgeCursors = ['ns-resize', 'ew-resize', 'ns-resize', 'ew-resize'];
|
|
for (let i = 0; i < 4; i++) {
|
|
const j = (i + 1) % 4;
|
|
const p1 = map.getPixelFromCoordinate(coords[i]);
|
|
const p2 = map.getPixelFromCoordinate(coords[j]);
|
|
if (pointToSegmentDist(pixel, p1 as unknown as number[], p2 as unknown as number[]) < EDGE_TOLERANCE) {
|
|
return { cursor: edgeCursors[i] };
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|