feat(B07 CAD): 337-2 마-1 오토캐드 마우스 — 누름 더함 · Shift 뺌 · 상자 더함/뺌 · 명령 중 끌기 상자 · 스침 · 그립 누르고-옮기고-누르기 · 그립 숫자 · 보기 전용 문 · 직교 강제 · Shift 임시 직교 · 휠 기본 밂 · 휠 두 번 범위
- 고르기: 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기 · 상자 = 앞 고름 ∪ 상자 · Shift 상자 = 뺌(H4 · H5) · 명령 안 고르기 단계도 누를 때 첫 점 · 스침 강조(ic/selecting.ts) - 그립: 쉴 때만 집음(N7) · 누르고 떼면 대기 → 다음 누름에 놓음 · 끌어 놓기(H3) · 거리 숫자(N8) · 확정 · 읽기 전용 도면은 그립 · Delete 막음(H2) - 직교: F8 = 기준점에서 가로 · 세로 강제 · Shift 누른 동안 뒤집음 · 객체 스냅 점 · 좌표 · 각도 잠금은 안 덮음 · 클릭 사건 snapped 채움(ic/ortho.ts · H8) · 가이드는 F3 와 뗌 · 가이드 잡는 거리 = 화면 픽셀(H9) - 휠: 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 모든 화면 · ☰ 설정 뒤집기 그대로) · 가운데 단추 두 번 = 범위 줌 · ZOOM · ⛶ 앞 화면 기억(ic/zoom-prompt.ts) - 시험: mouse-acad 새로 · 휠 방향 시험 손질(input-wheel · input-prefs-cad · input_prefs · app_settings · 164 · 220_1 · 221_1 · 244_8 기대값 뒤집음 · 266 pull 박음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다.
|
||||
* 클릭 한 번으로 그 그립을 옮긴다 (AutoCAD처럼 끌지 않고 집어서 놓는 방식).
|
||||
* 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다(뜨거운 그립).
|
||||
* 오토캐드처럼 누르고-옮기고-누르기 · 끌어 놓기 둘 다(누름 · 뗌 가름은 input-controller) ·
|
||||
* 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc = 그립 놓음.
|
||||
*/
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import { Actor, assign, createMachine } from 'xstate';
|
||||
@@ -63,7 +64,7 @@ export const gripEditToolStateMachine = createMachine(
|
||||
},
|
||||
[GripEditState.WAITING_FOR_TARGET_POINT]: {
|
||||
description: '그립을 옮길 위치를 지정한다',
|
||||
meta: { instructions: '그립을 옮길 위치를 지정하십시오' },
|
||||
meta: { instructions: '** 신축 ** 신축점 지정' },
|
||||
on: {
|
||||
[ActorEvent.DRAW]: { actions: GripEditAction.DRAW_TEMP_GRIP_EDIT },
|
||||
[ActorEvent.MOUSE_CLICK]: {
|
||||
@@ -75,6 +76,9 @@ export const gripEditToolStateMachine = createMachine(
|
||||
[ActorEvent.RELATIVE_POINT_INPUT]: {
|
||||
actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL],
|
||||
},
|
||||
[ActorEvent.NUMBER_INPUT]: {
|
||||
actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL],
|
||||
},
|
||||
[ActorEvent.ESC]: { actions: GripEditAction.SWITCH_TO_SELECT_TOOL },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,11 +22,15 @@ import {
|
||||
getLayers,
|
||||
getSelectedEntities,
|
||||
getSelectedEntityIds,
|
||||
getActiveToolActor,
|
||||
isDrawingReadOnly,
|
||||
isEntitySelected,
|
||||
isViewOnly,
|
||||
setEntities,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
} from '../state';
|
||||
import { Tool } from '../tools';
|
||||
import { startGripEdit } from './grip-edit-tool';
|
||||
import type { SelectContext } from './select-tool';
|
||||
import type { MouseClickEvent } from './tool.types';
|
||||
@@ -52,8 +56,11 @@ export function handleFirstSelectionPoint(
|
||||
context: SelectContext,
|
||||
event: MouseClickEvent
|
||||
): SelectContext {
|
||||
// 선택된 객체의 그립이 먼저다 — 집으면 그립 편집으로 넘어간다
|
||||
const gripHit = findGripAt(getSelectedEntities(), event.worldMouseLocation, pickRadius());
|
||||
// 선택된 객체의 그립이 먼저다 — 집으면 그립 편집으로 넘어간다.
|
||||
// 쉴 때(맨 위 = 선택 도구)만 · 명령 안 고르기는 그립 없음(N7) · 보기 전용 · 확정 도면은 막음(H2)
|
||||
const gripHit = gripsEditable()
|
||||
? findGripAt(getSelectedEntities(), event.worldMouseLocation, pickRadius())
|
||||
: null;
|
||||
if (gripHit) {
|
||||
if (event.holdingCtrl) {
|
||||
// Ctrl+클릭 => 폴리선 정점 제거 (다기능 그립)
|
||||
@@ -75,28 +82,10 @@ export function handleFirstSelectionPoint(
|
||||
|
||||
// Mouse is close to entity and is not dragging a rectangle
|
||||
if (pickedEntity) {
|
||||
// Select the entity close to the mouse
|
||||
const closestEntity = pickedEntity;
|
||||
if (!event.holdingCtrl && !event.holdingShift) {
|
||||
// 그룹으로 묶인 객체는 하나만 집어도 함께 선택된다 (GROUP)
|
||||
setSelectedEntityIds(expandSelectionWithGroups([closestEntity.id]));
|
||||
} else if (event.holdingCtrl) {
|
||||
// ctrl => toggle selection
|
||||
if (isEntitySelected(closestEntity)) {
|
||||
// Remove the entity from the selection
|
||||
setSelectedEntityIds(getSelectedEntityIds().filter((id) => id !== closestEntity.id));
|
||||
} else {
|
||||
// Add the entity to the selection
|
||||
setSelectedEntityIds(
|
||||
expandSelectionWithGroups([...getSelectedEntityIds(), closestEntity.id])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// shift => add to selection
|
||||
setSelectedEntityIds(
|
||||
expandSelectionWithGroups([...getSelectedEntityIds(), closestEntity.id])
|
||||
);
|
||||
}
|
||||
// 오토캐드 PICKADD 2 — 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기(그룹은 함께 · GROUP)
|
||||
const ids = expandSelectionWithGroups([pickedEntity.id]);
|
||||
const remove = event.holdingShift || (event.holdingCtrl && isEntitySelected(pickedEntity));
|
||||
setSelectedEntityIds(combineSelection(ids, remove ? 'remove' : 'add'));
|
||||
return {
|
||||
...context,
|
||||
startPoint: null,
|
||||
@@ -111,16 +100,37 @@ export function handleFirstSelectionPoint(
|
||||
};
|
||||
}
|
||||
|
||||
/** 앞 고름에 더함 · 뺌 · 넣고 빼기 */
|
||||
export function combineSelection(ids: string[], mode: 'add' | 'remove' | 'toggle'): string[] {
|
||||
const before = getSelectedEntityIds();
|
||||
if (mode === 'remove') return before.filter((id) => !ids.includes(id));
|
||||
if (mode === 'toggle') {
|
||||
return [
|
||||
...before.filter((id) => !ids.includes(id)),
|
||||
...ids.filter((id) => !before.includes(id)),
|
||||
];
|
||||
}
|
||||
return [...before, ...ids.filter((id) => !before.includes(id))];
|
||||
}
|
||||
|
||||
/** 그립 집기 · 그리기 — 쉴 때 · 고칠 수 있는 도면만 */
|
||||
export const gripsEditable = (): boolean =>
|
||||
!isViewOnly() &&
|
||||
!isDrawingReadOnly() &&
|
||||
getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT;
|
||||
|
||||
/** 상자 고르기 — 상자 = 앞 고름 ∪ 상자 · Shift = 앞 고름 − 상자 · Ctrl = 넣고 빼기(오토캐드 PICKADD 2) */
|
||||
export function selectEntitiesInsideRectangle(
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
holdingCtrl: boolean
|
||||
// holdingShift: boolean, // TODO implement add to selection using shift
|
||||
holdingCtrl: boolean,
|
||||
holdingShift = false
|
||||
): void {
|
||||
// Finish the selection
|
||||
const activeSelectionRectangle = new RectangleEntity(getActiveLayerId(), startPoint, endPoint);
|
||||
const intersectionSelection = getIsIntersectionSelection(activeSelectionRectangle, startPoint);
|
||||
const newSelectedEntityIds: string[] = compact(
|
||||
const box = activeSelectionRectangle.getBoundingBox() as Box;
|
||||
const boxIds: string[] = compact(
|
||||
getEntities().map((entity): string | null => {
|
||||
const layer = getLayers().find((layer) => layer.id === entity.layerId);
|
||||
if (!layer) {
|
||||
@@ -128,44 +138,17 @@ export function selectEntitiesInsideRectangle(
|
||||
console.error('Failed to find layer for entity', entity);
|
||||
return null;
|
||||
}
|
||||
if (intersectionSelection) {
|
||||
// Select all entities that are inside the selection rectangle or intersect with the selection rectangle
|
||||
if (
|
||||
entity.intersectsWithBox(activeSelectionRectangle.getBoundingBox() as Box) ||
|
||||
entity.isContainedInBox(activeSelectionRectangle.getBoundingBox() as Box)
|
||||
) {
|
||||
if (holdingCtrl) {
|
||||
if (isEntitySelected(entity)) {
|
||||
return null;
|
||||
}
|
||||
if (!layer.isLocked) {
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
if (!layer.isLocked) {
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Select only entities that are completely inside the selection rectangle
|
||||
if (entity.isContainedInBox(activeSelectionRectangle.getBoundingBox() as Box)) {
|
||||
if (holdingCtrl) {
|
||||
if (isEntitySelected(entity)) {
|
||||
return null;
|
||||
}
|
||||
if (!layer.isLocked) {
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
if (!layer.isLocked) {
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
if (layer.isLocked) return null;
|
||||
// 걸침(오→왼) = 걸치거나 든 것 · 창(왼→오) = 통째로 든 것만
|
||||
const inside =
|
||||
entity.isContainedInBox(box) || (intersectionSelection && entity.intersectsWithBox(box));
|
||||
return inside ? entity.id : null;
|
||||
})
|
||||
);
|
||||
setSelectedEntityIds(expandSelectionWithGroups(newSelectedEntityIds));
|
||||
const ids = expandSelectionWithGroups(boxIds);
|
||||
setSelectedEntityIds(
|
||||
combineSelection(ids, holdingShift ? 'remove' : holdingCtrl ? 'toggle' : 'add')
|
||||
);
|
||||
}
|
||||
|
||||
export function drawTempSelectionRectangle(startPoint: Point, endPoint: Point) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import {
|
||||
getNotSelectedEntities,
|
||||
isDrawingReadOnly,
|
||||
isViewOnly,
|
||||
setEntities,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
@@ -159,12 +161,13 @@ export const selectToolStateMachine = createMachine(
|
||||
selectEntitiesInsideRectangle(
|
||||
context.startPoint,
|
||||
(event as MouseClickEvent).worldMouseLocation,
|
||||
(event as MouseClickEvent).holdingCtrl
|
||||
// (event as MouseClickEvent).holdingShift,
|
||||
(event as MouseClickEvent).holdingCtrl,
|
||||
(event as MouseClickEvent).holdingShift
|
||||
);
|
||||
setGhostHelperEntities([]);
|
||||
},
|
||||
DELETE_SELECTED_ENTITIES: () => {
|
||||
if (isViewOnly() || isDrawingReadOnly()) return; // 보기 전용 · 확정 도면은 안 지움(H2)
|
||||
setEntities(getNotSelectedEntities(), true);
|
||||
setSelectedEntityIds([]);
|
||||
setGhostHelperEntities([]);
|
||||
|
||||
Reference in New Issue
Block a user