Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
1 change: 1 addition & 0 deletions README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) 호출됩니다.
Expand Down
89 changes: 54 additions & 35 deletions src/events/states/SelectionState.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const defaultConfig = {
onUp: () => {},
onClick: () => {},
onDoubleClick: () => {},
onRightClick: () => {},
onDragStart: () => {},
onDrag: () => {},
onDragEnd: () => {},
Expand All @@ -86,6 +87,7 @@ export default class SelectionState extends State {
'onpointerup',
'onpointerover',
'onclick',
'rightclick',
];

/** @type {SelectionStateConfig} */
Expand Down Expand Up @@ -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 });
}
Comment on lines +137 to +139

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the onpointerdown handler, when a right-click is detected (e.button === 2), you are clearing the gesture state by calling #clear with gesture: true. This nullifies this.dragStartPoint immediately after it has been set.

This causes an issue in the new #processClick method, which is used by the rightclick handler. The isMoved() check inside #processClick relies on this.dragStartPoint to determine if the cursor has moved between pointerdown and pointerup. Because dragStartPoint is null, isMoved() will always return false, and every right-click will be treated as a non-drag click, even if the user moved the mouse.

To fix this, you should avoid clearing the gesture state here. You only need to set the interactionState to IDLE to prevent dragging from starting. The gesture state will be correctly cleared at the end of the rightclick event processing within #processClick.

Suggested change
if (e.button === 2) {
this.#clear({ state: true, selectionBox: true, gesture: true });
}
if (e.button === 2) {
this.#clear({ state: true, selectionBox: true });
}

}

onpointermove(e) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};