/** * 사각형(Box) 리사이즈 커스텀 인터랙션 * * OL Modify는 사각형 제약을 지원하지 않으므로 PointerInteraction을 확장. * - 모서리 드래그: 대각 꼭짓점 고정, 자유 리사이즈 * - 변 드래그: 반대쪽 변 고정, 1축 리사이즈 */ import PointerInteraction from 'ol/interaction/Pointer'; const CORNER_TOLERANCE = 16; const EDGE_TOLERANCE = 12; /** 점(p)과 선분(a-b) 사이 최소 픽셀 거리 */ function pointToSegmentDist(p, a, b) { 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)); } export default class BoxResizeInteraction extends PointerInteraction { constructor(options) { super({ handleDownEvent: (evt) => BoxResizeInteraction.prototype._handleDown.call(this, evt), handleDragEvent: (evt) => BoxResizeInteraction.prototype._handleDrag.call(this, evt), handleUpEvent: (evt) => BoxResizeInteraction.prototype._handleUp.call(this, evt), }); this.feature_ = options.feature; this.onResize_ = options.onResize || null; // corner mode this.mode_ = null; // 'corner' | 'edge' this.draggedIndex_ = null; this.anchorCoord_ = null; // edge mode this.edgeIndex_ = null; this.bbox_ = null; } _handleDown(evt) { const pixel = evt.pixel; 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.draggedIndex_ = i; 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, p2) < EDGE_TOLERANCE) { this.mode_ = 'edge'; this.edgeIndex_ = i; const xs = coords.slice(0, 4).map(c => c[0]); const ys = coords.slice(0, 4).map(c => c[1]); this.bbox_ = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys), }; return true; } } return false; } _handleDrag(evt) { 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], ]]); } } _handleUp() { if (this.mode_) { this.mode_ = null; this.draggedIndex_ = null; this.anchorCoord_ = null; this.edgeIndex_ = null; this.bbox_ = null; if (this.onResize_) this.onResize_(this.feature_); return true; } return false; } /** * 호버 감지: 픽셀이 리사이즈 핸들 위인지 확인 * @returns {{ cursor: string }} | null */ isOverHandle(map, pixel) { 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, p2) < EDGE_TOLERANCE) { return { cursor: edgeCursors[i] }; } } return null; } }