Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-16 00:12:14 +09:00
13 changed files with 438 additions and 18 deletions
@@ -1,10 +1,11 @@
/**
* 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다(뜨거운 그립).
* 오토캐드처럼 누르고-옮기고-누르기 · 끌어 놓기 둘 다(누름 · 뗌 가름은 input-controller) ·
* 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc = 그립 놓음.
* 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc · 종료(X) = 그립 놓음.
*/
import type { Point } from '@flatten-js/core';
import { Actor, assign, createMachine } from 'xstate';
import type { OptionSpec, PromptSpec } from '../commands/prompt';
import type { Entity } from '../entities/Entity';
import { getPointFromEvent } from '../helpers/get-point-from-event';
import { type Grip, applyGrip } from '../helpers/grips';
@@ -20,6 +21,24 @@ import { Tool } from '../tools';
import { selectToolStateMachine } from './select-tool';
import { ActorEvent, type PointInputEvent, type StateEvent, type ToolContext } from './tool.types';
const gripOption = (key: string, kw: string, label: string): OptionSpec => ({
key,
kw,
en: key,
label,
});
/** 오토캐드 그립 신축 줄 — 종료(X)만 · 기준점(B) · 복사(C) · 명령 취소(U) 는 뺌 */
const GRIP_PROMPT: PromptSpec = {
text: '** 신축 ** 신축점 지정',
options: [gripOption('EXIT', 'X', '종료')],
absent: [
gripOption('BASE', 'B', '기준점'),
gripOption('COPY', 'C', '복사'),
gripOption('UNDO', 'U', '명령 취소'),
],
};
let editedEntity: Entity | null = null;
let editedGrip: Grip | null = null;
@@ -64,7 +83,7 @@ export const gripEditToolStateMachine = createMachine(
},
[GripEditState.WAITING_FOR_TARGET_POINT]: {
description: '그립을 옮길 위치를 지정한다',
meta: { instructions: '** 신축 ** 신축점 지정' },
meta: { kind: 'point', prompt: GRIP_PROMPT },
on: {
[ActorEvent.DRAW]: { actions: GripEditAction.DRAW_TEMP_GRIP_EDIT },
[ActorEvent.MOUSE_CLICK]: {
@@ -80,6 +99,7 @@ export const gripEditToolStateMachine = createMachine(
actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL],
},
[ActorEvent.ESC]: { actions: GripEditAction.SWITCH_TO_SELECT_TOOL },
'OPTION.EXIT': { actions: GripEditAction.SWITCH_TO_SELECT_TOOL },
},
},
},
@@ -31,6 +31,7 @@ import {
setSelectedEntityIds,
} from '../state';
import { Tool } from '../tools';
import { previousSelection } from '../commands/prompt';
import { startGripEdit } from './grip-edit-tool';
import type { SelectContext } from './select-tool';
import type { MouseClickEvent } from './tool.types';
@@ -113,6 +114,26 @@ export function combineSelection(ids: string[], mode: 'add' | 'remove' | 'toggle
return [...before, ...ids.filter((id) => !before.includes(id))];
}
/** 알림 줄 — 입력 층이 붙임(`ic/selecting.ts` · prompt-input 을 바로 부르면 불러오기 고리) */
let notice: (text: string) => void = () => {};
export const setKeywordNotice = (sink: (text: string) => void): void => {
notice = sink;
};
/** 고르기 키워드 — 전체(ALL = Ctrl+A 와 같은 목록) · 최종(LAST = 집을 수 있는 마지막) · 이전(P) · 앞 고름에 더함 */
export function selectByKeyword(key: 'ALL' | 'LAST' | 'PREVIOUS'): void {
const pickable = getPickableEntities();
const found =
key === 'ALL'
? pickable.map((entity) => entity.id)
: key === 'LAST'
? pickable.slice(-1).map((entity) => entity.id)
: previousSelection().filter((id) => pickable.some((entity) => entity.id === id));
const ids = combineSelection(expandSelectionWithGroups(found), 'add');
setSelectedEntityIds(ids);
notice(`${found.length}개를 찾음, 총 ${ids.length}개`);
}
/** 그립 집기 · 그리기 — 쉴 때 · 고칠 수 있는 도면만 */
export const gripsEditable = (): boolean =>
!isViewOnly() &&
@@ -14,6 +14,7 @@ import { assign, createMachine } from 'xstate';
import {
drawTempSelectionRectangle,
handleFirstSelectionPoint,
selectByKeyword,
selectEntitiesInsideRectangle,
} from './select-tool.helpers';
@@ -76,6 +77,10 @@ export const selectToolStateMachine = createMachine(
actions: SelectAction.DELETE_SELECTED_ENTITIES,
target: SelectState.INIT,
},
// 고르기 숨은 키워드(SELECT_OBJECTS) — 전체 · 최종 · 이전
'OPTION.ALL': { actions: () => selectByKeyword('ALL') },
'OPTION.LAST': { actions: () => selectByKeyword('LAST') },
'OPTION.PREVIOUS': { actions: () => selectByKeyword('PREVIOUS') },
},
},
[SelectState.CHECK_SELECTION]: {