diff --git a/README.md b/README.md index 0751c634..d7112b26 100644 --- a/README.md +++ b/README.md @@ -501,6 +501,7 @@ The default state that handles user selection and drag events. It is automatical - `onUp` (optional, function): Callback fired on `pointerup` if it was not a drag operation. - `onClick` (optional, function): Callback fired when a complete 'click' is detected. This will not fire if `onDoubleClick` fires. - `onDoubleClick` (optional, function): Callback fired when a complete 'double-click' is detected. Based on `e.detail === 2`. +- `onRightClick` (optional, function): Callback fired when a complete right-click is detected. The browser's default context menu is automatically prevented within the canvas area. - `onDragStart` (optional, function): Callback fired *once* when a drag operation (for multi-selection) begins (after moving beyond a threshold). - `onDrag` (optional, function): Callback fired repeatedly *during* a drag operation. - `onDragEnd` (optional, function): Callback fired when the drag operation *ends* (`pointerup`). diff --git a/README_KR.md b/README_KR.md index 5d9cd31c..21ef8c12 100644 --- a/README_KR.md +++ b/README_KR.md @@ -509,6 +509,7 @@ patchmap.stateManager.setState('custom', { message: 'Hello World' }); - `onUp` (optional, function): 드래그가 아닐 경우, `pointerup` 시점에 호출됩니다. - `onClick` (optional, function): '클릭'이 '완료'되었을 때 호출됩니다. 더블클릭이 아닐 때만 호출됩니다. - `onDoubleClick` (optional, function): '더블클릭'이 '완료'되었을 때 호출됩니다. `e.detail === 2`를 기반으로 호출됩니다. +- `onRightClick` (optional, function): '우클릭'이 '완료'되었을 때 호출됩니다. 캔버스 영역 내에서 브라우저 기본 컨텍스트 메뉴가 나타나지 않도록 자동으로 방지됩니다. - `onDragStart` (optional, function): 드래그(다중 선택)가 '시작'되는 시점 (일정 거리 이상 이동)에 1회 호출됩니다. - `onDrag` (optional, function): 드래그가 '진행'되는 동안 실시간으로 호출됩니다. - `onDragEnd` (optional, function): 드래그가 '종료'되었을 때 (`pointerup`) 호출됩니다. diff --git a/src/events/states/SelectionState.js b/src/events/states/SelectionState.js index ab789723..91e61d98 100644 --- a/src/events/states/SelectionState.js +++ b/src/events/states/SelectionState.js @@ -69,6 +69,7 @@ const defaultConfig = { onUp: () => {}, onClick: () => {}, onDoubleClick: () => {}, + onRightClick: () => {}, onDragStart: () => {}, onDrag: () => {}, onDragEnd: () => {}, @@ -86,6 +87,7 @@ export default class SelectionState extends State { 'onpointerup', 'onpointerover', 'onclick', + 'rightclick', ]; /** @type {SelectionStateConfig} */ @@ -129,8 +131,12 @@ export default class SelectionState extends State { this.dragStartPoint = this.viewport.toWorld(e.global); this._lastPaintPoint = this.dragStartPoint; - const target = this.#searchObject(this.dragStartPoint, e); + const target = this.#searchObject(this.dragStartPoint, e, true); this.config.onDown(target, e); + + if (e.button === 2) { + this.#clear({ state: true, selectionBox: true, gesture: true }); + } } onpointermove(e) { @@ -202,48 +208,61 @@ export default class SelectionState extends State { } onclick(e) { - if (this.movedViewport) { - this.#clear({ gesture: true }); - return; - } - - const currentPoint = this.viewport.toWorld(e.global); - if (isMoved(this.dragStartPoint, currentPoint, this.viewport.scale)) { - this.#clear({ gesture: true }); - return; - } + this.#processClick(e, (target, currentPoint) => { + if (this.config.drillDown && e.detail >= 2) { + for (let i = 1; i < e.detail; i++) { + if (!target) break; + const deeperTarget = findIntersectObject( + target, + currentPoint, + this.config, + ); + if (!deeperTarget) break; + target = deeperTarget; + } + } - let target = this.#searchObject(currentPoint, e); - if (this.config.drillDown && e.detail >= 2) { - for (let i = 1; i < e.detail; i++) { - if (!target) break; - const deeperTarget = findIntersectObject( - target, - currentPoint, - this.config, - ); - if (!deeperTarget) break; - target = deeperTarget; + if (e.detail === 2) { + this.config.onDoubleClick(target, e); + } else { + this.config.onClick(target, e); } - } + }); + } + + rightclick(e) { + this.#processClick(e, (target) => { + this.config.onRightClick(target, e); + }); + } + + #processClick(e, callback) { + const currentPoint = this.viewport.toWorld(e.global); + const isActuallyMoved = + this.movedViewport || + isMoved(this.dragStartPoint, currentPoint, this.viewport.scale); - if (e.detail === 2) { - this.config.onDoubleClick(target, e); - } else { - this.config.onClick(target, e); + if (!isActuallyMoved) { + const target = this.#searchObject(currentPoint, e); + callback(target, currentPoint); } this.#clear({ gesture: true }); } - #searchObject(point, e) { + #searchObject(point, e, skipWireframeCheck) { if (this.config.deepSelect && (e.ctrlKey || e.metaKey)) { - return this.#findByPoint(point, { ...this.config, selectUnit: 'grid' }); + return this.#findByPoint( + point, + { ...this.config, selectUnit: 'grid' }, + skipWireframeCheck, + ); } - return this.#findByPoint(point, { - ...this.config, - filterParent: this.#getSelectionAncestors(), - }); + return this.#findByPoint( + point, + { ...this.config, filterParent: this.#getSelectionAncestors() }, + skipWireframeCheck, + ); } #searchObjects(polygon) { @@ -253,9 +272,9 @@ export default class SelectionState extends State { }); } - #findByPoint(point, config = this.config) { + #findByPoint(point, config = this.config, skipWireframeCheck = false) { const object = findIntersectObject(this.viewport, point, config); - if (!object || object.type !== 'wireframe') { + if (skipWireframeCheck || !object || object.type !== 'wireframe') { return object; } diff --git a/src/init.js b/src/init.js index 73885a82..9b49f22f 100644 --- a/src/init.js +++ b/src/init.js @@ -141,6 +141,7 @@ export const initResizeObserver = (el, app, viewport) => { export const initCanvas = (el, app) => { const div = document.createElement('div'); div.style = 'width:100%;height:100%;overflow:hidden;'; + div.oncontextmenu = (e) => e.preventDefault(); div.appendChild(app.canvas); el.appendChild(div); };