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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,11 @@ The selection event is activated to detect objects that the user selects on the
This should be executed after the `draw` method.
- `enabled` (optional, boolean): Determines whether the selection event is enabled.
- `draggable` (optional, boolean): Determines whether dragging is enabled.
- `isSelectGroup` (optional, boolean): Decides whether to select group objects.
- `isSelectGrid` (optional, boolean): Decides whether to select grid objects.
- `selectUnit` (optional, string): Specifies the logical unit to return when selecting. The default is `'entity'`.
- `'entity'`: Selects individual objects.
- `'closestGroup'`: Selects the closest parent group of the selected object.
- `'highestGroup'`: Selects the highest-level group of the selected object.
- `'grid'`: Selects the grid to which the selected object belongs.
- `filter` (optional, function): A function that filters the target objects based on specific conditions.
- `onSelect` (optional, function): The callback function that is called when a selection occurs.
- `onOver` (optional, function): The callback function that is called when a pointer-over event occurs.
Expand All @@ -418,8 +421,7 @@ This should be executed after the `draw` method.
patchmap.select({
enabled: true,
draggable: true,
isSelectGroup: false,
isSelectGrid: true,
selectUnit: 'grid',
filter: (obj) => obj.type !== 'relations',
onSelect: (obj) => {
console.log(obj);
Expand Down
10 changes: 6 additions & 4 deletions README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,11 @@ const result = patchmap.selector('$..[?(@.label=="group-label-1")]')
`draw` 메소드 이후에 실행되어야 합니다.
- `enabled` (optional, boolean): 선택 이벤트의 활성화 여부를 결정합니다.
- `draggable` (optional, boolean): 드래그 활성화 여부를 결정합니다.
- `isSelectGroup` (optional, boolean): group 객체를 선택할지 결정합니다.
- `isSelectGrid` (optional, boolean): grid 객체를 선택할지 결정합니다.
- `selectUnit` (optional, string): 선택 시 반환될 논리적 단위를 지정합니다. 기본값은 'entity' 입니다.
- `'entity'`: 개별 객체를 선택합니다.
- `'closestGroup'`: 선택된 객체에서 가장 가까운 상위 그룹을 선택합니다.
- `'highestGroup'`: 선택된 객체에서 가장 최상위 그룹을 선택합니다.
- `'grid'`: 선택된 객체가 속한 그리드를 선택합니다.
- `filter` (optional, function): 선택 대상 객체를 조건에 따라 필터링할 수 있는 함수입니다.
- `onSelect` (optional, function): 선택이 발생할 때 호출될 콜백 함수입니다.
- `onOver` (optional, function): 포인터 오버가 발생할 때 호출될 콜백 함수입니다.
Expand All @@ -417,8 +420,7 @@ const result = patchmap.selector('$..[?(@.label=="group-label-1")]')
patchmap.select({
enabled: true,
draggable: true,
isSelectGroup: false,
isSelectGrid: true,
selectUnit: 'grid',
filter: (obj) => obj.type !== 'relations',
onSelect: (obj) => {
console.log(obj);
Expand Down
3 changes: 3 additions & 0 deletions src/display/elements/Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { mixins } from '../mixins/utils';
const ComposedElement = mixins(Container, Base, Showable);

export default class Element extends ComposedElement {
static isSelectable = false;
static hitScope = 'self'; // 'self' | 'children'

constructor(options) {
super(Object.assign(options, { eventMode: 'static' }));
}
Expand Down
3 changes: 3 additions & 0 deletions src/display/elements/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import Element from './Element';
const ComposedItem = mixins(Element, Componentsable, ItemSizeable);

export class Item extends ComposedItem {
static isSelectable = true;
static hitScope = 'children';

constructor(context) {
super({ type: 'item', context });
}
Expand Down
7 changes: 5 additions & 2 deletions src/display/elements/Relations.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Element from './Element';
const ComposedRelations = mixins(Element, Linksable, Relationstyleable);

export class Relations extends ComposedRelations {
static isSelectable = true;
static hitScope = 'children';

_renderDirty = true;
_renderOnNextTick = false;

Expand Down Expand Up @@ -69,10 +72,10 @@ export class Relations extends ComposedRelations {
continue;
}

const sourceBounds = this.toLocal(
const sourceBounds = this.context.viewport.toLocal(
calcOrientedBounds(sourceObject).center,
);
const targetBounds = this.toLocal(
const targetBounds = this.context.viewport.toLocal(
calcOrientedBounds(targetObject).center,
);

Expand Down
18 changes: 7 additions & 11 deletions src/events/drag-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { event } from '../utils/event/canvas';
import { validate } from '../utils/validator';
import { findIntersectObjects } from './find';
import { dragSelectEventSchema } from './schema';
import { checkEvents, getPointerPosition, isMoved } from './utils';
import { checkEvents, isMoved } from './utils';

const DRAG_SELECT_EVENT_ID = 'drag-select-down drag-select-move drag-select-up';
const DEBOUNCE_FN_INTERVAL = 25; // ms
Expand Down Expand Up @@ -39,10 +39,10 @@ const addEvents = (viewport, state) => {
event.addEvent(viewport, {
id: 'drag-select-down',
action: 'mousedown touchstart',
fn: () => {
fn: (e) => {
resetState(state);

const point = getPointerPosition(viewport);
const point = viewport.toWorld({ ...e.global });
state.isDragging = true;
state.box.renderable = true;
state.point.start = { ...point };
Expand All @@ -56,9 +56,9 @@ const addEvents = (viewport, state) => {
id: 'drag-select-move',
action: 'mousemove touchmove moved',
fn: (e) => {
if (!state.isDragging) return;
if (!state.isDragging || !e.global) return;

state.point.end = { ...getPointerPosition(viewport) };
state.point.end = viewport.toWorld({ ...e.global });
drawSelectionBox(state);

if (isMoved(viewport, state.point.move, state.point.end)) {
Expand Down Expand Up @@ -94,14 +94,10 @@ const drawSelectionBox = (state) => {
if (!point.start || !point.end) return;

box.clear();
box.position.set(
Math.min(point.start.x, point.end.x),
Math.min(point.start.y, point.end.y),
);
box
.rect(
0,
0,
Math.min(point.start.x, point.end.x),
Math.min(point.start.y, point.end.y),
Math.abs(point.start.x - point.end.x),
Math.abs(point.start.y - point.end.y),
)
Expand Down
111 changes: 74 additions & 37 deletions src/events/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,96 @@ import { intersectPoint } from '../utils/intersects/intersect-point';
import { getSelectObject } from './utils';

export const findIntersectObject = (viewport, state, options) => {
return searchIntersect(viewport);

function searchIntersect(parent) {
const children = [...parent.children].sort((a, b) => {
const zDiff = (b.zIndex || 0) - (a.zIndex || 0);
if (zDiff !== 0) return zDiff;
return parent.getChildIndex(b) - parent.getChildIndex(a);
});

for (const child of children) {
if (
child.renderPipeId ||
child.type === 'item' ||
(options.isSelectGrid && child.type === 'grid')
) {
const isIntersecting = intersectPoint(child, state.point);
const selectObject = isIntersecting
? getSelectObject(child, options)
: null;
const allCandidates = collectCandidates(
viewport,
(child) => child.constructor.isSelectable,
);

const sortedCandidates = allCandidates.sort((a, b) => {
const zDiff = (b.zIndex || 0) - (a.zIndex || 0);
if (zDiff !== 0) return zDiff;

const pathA = getAncestorPath(a, viewport);
const pathB = getAncestorPath(b, viewport);

const minLength = Math.min(pathA.length, pathB.length);
for (let i = 0; i < minLength; i++) {
if (pathA[i] !== pathB[i]) {
const commonParent = pathA[i].parent;
return (
commonParent.getChildIndex(pathB[i]) -
commonParent.getChildIndex(pathA[i])
);
}
}
return pathB.length - pathA.length;
});

for (const candidate of sortedCandidates) {
const targets =
candidate.constructor.hitScope === 'children'
? candidate.children
: [candidate];

for (const target of targets) {
const isIntersecting = intersectPoint(target, state.point);
if (isIntersecting) {
const selectObject = getSelectObject(candidate, options);
if (selectObject && (!options.filter || options.filter(selectObject))) {
return selectObject;
}
}

const found = searchIntersect(child);
if (found) return found;
}
return null;
}

return null;
};

export const findIntersectObjects = (viewport, state, options) => {
return Array.from(new Set(searchIntersect(viewport)));

function searchIntersect(parent) {
let found = [];
const children = [...parent.children];
const allCandidates = collectCandidates(
viewport,
(child) => child.constructor.isSelectable,
);
const found = [];

for (const child of children) {
if (child.renderPipeId || ['item', 'relations'].includes(child.type)) {
const isIntersecting = intersect(state.box, child);
const selectObject = isIntersecting
? getSelectObject(child, options)
: null;
for (const candidate of allCandidates) {
const targets =
candidate.constructor.hitScope === 'children'
? candidate.children
: [candidate];

for (const target of targets) {
const isIntersecting = intersect(state.box, target);
if (isIntersecting) {
const selectObject = getSelectObject(candidate, options);
if (selectObject && (!options.filter || options.filter(selectObject))) {
found.push(selectObject);
break;
}
} else {
found = found.concat(searchIntersect(child));
}
}
return found;
}

return Array.from(new Set(found));
};

const collectCandidates = (parent, filterFn) => {
let candidates = [];
for (const child of parent.children) {
if (filterFn(child)) {
candidates.push(child);
}
candidates = candidates.concat(collectCandidates(child, filterFn));
}
return candidates;
};

const getAncestorPath = (obj, stopAt) => {
const path = [];
let current = obj;
while (current && current !== stopAt) {
path.unshift(current);
current = current.parent;
}
return path;
};
5 changes: 3 additions & 2 deletions src/events/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { z } from 'zod';
const selectDefaultSchema = z.object({
enabled: z.boolean().default(false),
filter: z.nullable(z.function()).default(null),
isSelectGroup: z.boolean().default(false),
isSelectGrid: z.boolean().default(false),
selectUnit: z
.enum(['entity', 'closestGroup', 'highestGroup', 'grid'])
.default('entity'),
});

export const selectEventSchema = selectDefaultSchema.extend({
Expand Down
16 changes: 5 additions & 11 deletions src/events/single-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { event } from '../utils/event/canvas';
import { validate } from '../utils/validator';
import { findIntersectObject } from './find';
import { selectEventSchema } from './schema';
import { checkEvents, getPointerPosition, isMoved } from './utils';
import { checkEvents, isMoved } from './utils';

const SELECT_EVENT_ID = 'select-down select-up select-over';

Expand All @@ -30,11 +30,8 @@ const addEvents = (viewport, state) => {
event.addEvent(viewport, {
id: 'select-down',
action: 'mousedown touchstart',
fn: () => {
state.position.start = {
x: viewport.position.x,
y: viewport.position.y,
};
fn: (e) => {
state.position.start = viewport.toWorld({ ...e.global });
},
});
}
Expand All @@ -44,10 +41,7 @@ const addEvents = (viewport, state) => {
id: 'select-up',
action: 'mouseup touchend',
fn: (e) => {
state.position.end = {
x: viewport.position.x,
y: viewport.position.y,
};
state.position.end = viewport.toWorld({ ...e.global });

if (
state.position.start &&
Expand All @@ -73,7 +67,7 @@ const addEvents = (viewport, state) => {
}

function executeFn(fnName, e) {
const point = getPointerPosition(viewport);
const point = viewport.toWorld({ ...e.global });
if (fnName in state.config) {
state.config[fnName](
findIntersectObject(viewport, { point }, state.config),
Expand Down
Loading