Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/** 문자 명령 — 여러 줄 문자·단일 행 문자·편집·찾기 (조사표 5절 문자 패널) */
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { Point, Vector } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getAnnotationScale } from '../../commands/dim-settings';
|
||||
import { type OptionSpec, type PromptSpec, recall } from '../../commands/prompt';
|
||||
import { EntityName } from '../../entities/Entity';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import type { TextEntity } from '../../entities/TextEntity';
|
||||
import {
|
||||
addEntities,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
setEntities,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { textEntity } from '../factories/entity-factory';
|
||||
import { polyLineEntity, textEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
|
||||
/** AutoCAD 여러 줄 문자의 줄바꿈 표기(\P)와 \n을 모두 받는다 */
|
||||
@@ -24,42 +24,156 @@ const splitLines = (text: string): string[] =>
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
function buildTextLines(lines: string[], basePoint: Point): Entity[] {
|
||||
const lineHeight = getActiveTextStyle().fontSize * getAnnotationScale() * 1.35;
|
||||
const groupId = lines.length > 1 ? crypto.randomUUID() : undefined;
|
||||
return lines.map((line, index) => {
|
||||
const entity = textEntity(line, new Point(basePoint.x, basePoint.y - lineHeight * index), {
|
||||
textAlign: 'left',
|
||||
fontSize: getActiveTextStyle().fontSize * getAnnotationScale(),
|
||||
});
|
||||
entity.groupId = groupId;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
/** 줄 간격 = 글자 높이 × 1.35 */
|
||||
const LINE_GAP = 1.35;
|
||||
const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label });
|
||||
/** 지금 글자 크기 — TEXTSIZE 처음 값 */
|
||||
const textSize = () => getActiveTextStyle().fontSize * getAnnotationScale();
|
||||
const TEXT_HEIGHT = (): PromptSpec => ({
|
||||
text: '높이 지정',
|
||||
default: { kind: 'value', remember: 'TEXTSIZE', initial: textSize() },
|
||||
number: { positive: true },
|
||||
});
|
||||
/** 글 물음 — 빈 Enter = 끝(기본은 안 그림) */
|
||||
const typing = (text: string): PromptSpec => ({ text, default: { kind: 'option', key: 'DONE' } });
|
||||
|
||||
/** 한 줄 글 — 높이 · 회전 각(도) */
|
||||
const placeText = (label: string, point: Point, height: number, angle: number) => {
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
return textEntity(label, point, {
|
||||
textAlign: 'left',
|
||||
fontSize: height,
|
||||
textDirection: new Vector(Math.cos(radians), Math.sin(radians)),
|
||||
});
|
||||
};
|
||||
|
||||
type TextLine = { point: Point; height: number; angle: number; start: Point | null };
|
||||
/** TEXT 마지막 줄(세션) — 다음 줄 · 시작점 Enter 가 그 아래로 이어 씀 */
|
||||
let lastLine: TextLine | null = null;
|
||||
|
||||
const below = ({ point, height, angle }: TextLine) => {
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
const gap = height * LINE_GAP;
|
||||
return new Point(point.x + Math.sin(radians) * gap, point.y - Math.cos(radians) * gap);
|
||||
};
|
||||
|
||||
/**
|
||||
* TEXT — 「문자의 시작점 지정:」 → 「높이 지정 <h>:」 → 「문자의 회전 각도 지정 <0>:」 → 줄마다 「문자 입력:」.
|
||||
* 줄 Enter = 그 줄 만들고 아래 새 줄 · 빈 줄 Enter = 끝 · 시작점 Enter = 마지막 글 아래(높이 · 각 안 물음).
|
||||
*/
|
||||
export const textToolStateMachine = createSequenceTool({
|
||||
tool: Tool.TEXT,
|
||||
loopFrom: 'line',
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '문자의 시작점을 지정하십시오.' },
|
||||
{ kind: 'text', instructions: '문자를 입력하십시오.' },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'start',
|
||||
prompt: () => ({
|
||||
text: '문자의 시작점 지정',
|
||||
absent: [opt('JUSTIFY', 'J', '자리맞추기'), opt('STYLE', 'S', '스타일')],
|
||||
...(lastLine ? { default: { kind: 'option', key: 'CONTINUE' } as const } : {}),
|
||||
}),
|
||||
options: { CONTINUE: { mode: 'continue', next: 'line' } },
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'height',
|
||||
when: (input) => input.mode !== 'continue',
|
||||
prompt: TEXT_HEIGHT,
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'angle',
|
||||
when: (input) => input.mode !== 'continue',
|
||||
prompt: {
|
||||
text: '문자의 회전 각도 지정',
|
||||
default: { kind: 'value', remember: 'TEXT.ANGLE', initial: 0, format: 'ang' },
|
||||
},
|
||||
},
|
||||
{ kind: 'text', id: 'line', prompt: typing('문자 입력'), options: { DONE: 'end' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const lines = splitLines(input.text(1));
|
||||
if (!lines.length) return;
|
||||
addEntities(buildTextLines(lines.slice(0, 1), input.point(0)), true);
|
||||
const start = input.has('start') ? input.get<Point>('start') : null;
|
||||
const previous = start ? null : lastLine;
|
||||
const height = previous?.height ?? input.get<number>('height');
|
||||
const angle = previous?.angle ?? input.get<number>('angle');
|
||||
// 같은 명령의 다음 줄 · 시작점 Enter = 마지막 줄 아래
|
||||
const follow = lastLine && (!start || lastLine.start === start) ? lastLine : null;
|
||||
const point = follow ? below(follow) : (start as Point);
|
||||
addEntities([placeText(String(input.get('line')), point, height, angle)], true);
|
||||
lastLine = { point, height, angle, start };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* MTEXT — 「첫 번째 구석 지정:」 → 「반대 구석 지정 또는 [높이(H)/폭(W)]:」 → 「문자 입력:」(줄바꿈 \P).
|
||||
* 첫 줄 = 상자 왼쪽 위 · 줄 감기 없음(줄은 \P 로만).
|
||||
*/
|
||||
export const mtextToolStateMachine = createSequenceTool({
|
||||
tool: Tool.MTEXT,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '여러 줄 문자의 첫 코너를 지정하십시오.' },
|
||||
{ kind: 'text', instructions: '문자를 입력하십시오 (줄바꿈은 \\P).' },
|
||||
{ kind: 'point', id: 'corner', prompt: { text: '첫 번째 구석 지정' } },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'opposite',
|
||||
prompt: {
|
||||
text: '반대 구석 지정',
|
||||
options: [opt('HEIGHT', 'H', '높이'), opt('WIDTH', 'W', '폭')],
|
||||
absent: [
|
||||
opt('JUSTIFY', 'J', '자리맞추기'),
|
||||
opt('LINESPACING', 'L', '행 간격'),
|
||||
opt('ROTATION', 'R', '회전'),
|
||||
opt('STYLE', 'S', '스타일'),
|
||||
opt('COLUMNS', 'C', '열'),
|
||||
],
|
||||
},
|
||||
options: { HEIGHT: { mode: 'H', next: 'height' }, WIDTH: { mode: 'W', next: 'width' } },
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'height',
|
||||
when: (input) => !input.has('opposite'),
|
||||
next: 'opposite',
|
||||
prompt: TEXT_HEIGHT,
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'width',
|
||||
when: (input) => !input.has('opposite'),
|
||||
prompt: { text: '폭 지정', number: { positive: true } },
|
||||
},
|
||||
{
|
||||
kind: 'text',
|
||||
id: 'text',
|
||||
prompt: typing('문자 입력 (줄바꿈 \\P)'),
|
||||
options: { DONE: 'end' },
|
||||
},
|
||||
],
|
||||
preview: (input) => {
|
||||
const corner = input.get<Point>('corner');
|
||||
if (!corner || input.has('opposite') || input.mode === 'W') return [];
|
||||
const { x, y } = input.cursor;
|
||||
const box = [corner, new Point(x, corner.y), new Point(x, y), new Point(corner.x, y), corner];
|
||||
const outline = polyLineEntity(box);
|
||||
return outline ? [outline] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const lines = splitLines(input.text(1));
|
||||
const lines = splitLines(String(input.get('text')));
|
||||
if (!lines.length) return;
|
||||
addEntities(buildTextLines(lines, input.point(0)), true);
|
||||
const corner = input.get<Point>('corner');
|
||||
const opposite = input.has('opposite') ? input.get<Point>('opposite') : corner;
|
||||
const height = input.has('height')
|
||||
? input.get<number>('height')
|
||||
: (recall<number>('TEXTSIZE') ?? textSize());
|
||||
const left = Math.min(corner.x, opposite.x);
|
||||
const top = Math.max(corner.y, opposite.y) - height;
|
||||
const groupId = lines.length > 1 ? crypto.randomUUID() : undefined;
|
||||
const entities = lines.map((line, index) => {
|
||||
const entity = placeText(line, new Point(left, top - height * LINE_GAP * index), height, 0);
|
||||
entity.groupId = groupId;
|
||||
return entity;
|
||||
});
|
||||
addEntities(entities, true);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import { getMlineElements, getMlineSpacing, setMlineStyle } from '../../commands/draw-settings';
|
||||
import type { OptionSpec, PromptSpec } from '../../commands/prompt';
|
||||
import { offsetPolylinePoints } from '../../helpers/geometry/shape-points';
|
||||
import { addEntities, getScreenCanvasDrawController } from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { hatchEntity, lineEntity, polyLineEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool';
|
||||
|
||||
/**
|
||||
* 구성선은 원래 무한하지만, 무한 선은 경계상자를 망가뜨려 [범위 줌]을 못 쓰게 만든다.
|
||||
@@ -32,30 +33,75 @@ function extendFrom(base: Point, through: Point, bothWays: boolean): [Point, Poi
|
||||
return [backward, forward];
|
||||
}
|
||||
|
||||
const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label });
|
||||
|
||||
/** 통과점 물음 — 빈 Enter = 끝(기본은 안 그림) */
|
||||
const THROUGH: PromptSpec = { text: '통과점을 지정', default: { kind: 'option', key: 'DONE' } };
|
||||
|
||||
/** 구성선 양 끝 — 수평 · 수직 · 각도 모드는 통과점 하나로 · 아니면 첫 점 → 통과점 방향 */
|
||||
function xlineEnds(input: SequenceInput, through: Point): [Point, Point] | null {
|
||||
const degrees = { H: 0, V: 90, A: input.has('angle') ? input.get<number>('angle') : 0 }[
|
||||
input.mode ?? ''
|
||||
];
|
||||
if (degrees === undefined) {
|
||||
const base = input.get<Point>('base');
|
||||
if (!base || (base.x === through.x && base.y === through.y)) return null;
|
||||
return extendFrom(base, through, true);
|
||||
}
|
||||
const radians = (degrees * Math.PI) / 180;
|
||||
const toward = new Point(through.x + Math.cos(radians), through.y + Math.sin(radians));
|
||||
return extendFrom(through, toward, true);
|
||||
}
|
||||
|
||||
/** XLINE — 오토캐드 「점 지정 또는 [수평(H)/수직(V)/각도(A)]:」 → 「통과점을 지정:」 되풀이 */
|
||||
export const xlineToolStateMachine = createSequenceTool({
|
||||
tool: Tool.XLINE,
|
||||
loopFrom: 'through',
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '구성선이 지날 점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '방향을 지정하십시오.' },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'base',
|
||||
prompt: {
|
||||
text: '점 지정',
|
||||
options: [opt('HOR', 'H', '수평'), opt('VER', 'V', '수직'), opt('ANG', 'A', '각도')],
|
||||
absent: [opt('BISECT', 'B', '이등분'), opt('OFFSET', 'O', '간격띄우기')],
|
||||
},
|
||||
options: {
|
||||
HOR: { mode: 'H', next: 'through' },
|
||||
VER: { mode: 'V', next: 'through' },
|
||||
ANG: { mode: 'A', next: 'angle' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'angle',
|
||||
when: (input) => input.mode === 'A',
|
||||
prompt: {
|
||||
text: 'X선의 각도 입력',
|
||||
default: { kind: 'value', remember: 'XLINE.ANGLE', initial: 0, format: 'ang' },
|
||||
absent: [opt('REFERENCE', 'R', '참조')],
|
||||
},
|
||||
},
|
||||
{ kind: 'point', id: 'through', prompt: THROUGH, options: { DONE: 'end' } },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length !== 1) return [];
|
||||
const [start, end] = extendFrom(points[0], input.cursor, true);
|
||||
return [lineEntity(start, end)];
|
||||
if (!input.mode && !input.has('base')) return [];
|
||||
const ends = xlineEnds(input, input.cursor);
|
||||
return ends ? [lineEntity(...ends)] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [base, through] = input.points();
|
||||
const [start, end] = extendFrom(base, through, true);
|
||||
addEntities([lineEntity(start, end)], true);
|
||||
const ends = xlineEnds(input, input.get<Point>('through'));
|
||||
if (ends) addEntities([lineEntity(...ends)], true);
|
||||
},
|
||||
});
|
||||
|
||||
/** RAY — 「시작점을 지정:」 → 「통과점을 지정:」 되풀이 */
|
||||
export const rayToolStateMachine = createSequenceTool({
|
||||
tool: Tool.RAY,
|
||||
loopFrom: 'through',
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '광선의 시작점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '통과점을 지정하십시오.' },
|
||||
{ kind: 'point', id: 'base', prompt: { text: '시작점을 지정' } },
|
||||
{ kind: 'point', id: 'through', prompt: THROUGH, options: { DONE: 'end' } },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
|
||||
@@ -1,17 +1,44 @@
|
||||
/** 해치·그라데이션·경계·영역 — 선택한 객체가 이루는 닫힌 경계를 채우거나 뽑는다 */
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { assign, createMachine, sendTo } from 'xstate';
|
||||
import {
|
||||
autoHatchSpacing,
|
||||
getHatchAngle,
|
||||
getHatchSpacing,
|
||||
getHatchStyle,
|
||||
} from '../../commands/draw-settings';
|
||||
import {
|
||||
OPT,
|
||||
type OptionSpec,
|
||||
type PromptSpec,
|
||||
SELECT_OBJECTS,
|
||||
rememberSelection,
|
||||
} from '../../commands/prompt';
|
||||
import { entitiesToLoop } from '../../helpers/geometry/entity-loop';
|
||||
import { addEntities, getActiveLineColor, setSelectedEntityIds } from '../../state';
|
||||
import { isPointInPolygon } from '../../helpers/geometry/hatch-lines';
|
||||
import { sampleEntityPoints } from '../../helpers/geometry/sample-entity';
|
||||
import {
|
||||
addEntities,
|
||||
getActiveLineColor,
|
||||
getPickableEntities,
|
||||
getSelectedEntities,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { hatchEntity, polyLineEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool';
|
||||
import { selectToolStateMachine } from '../select-tool';
|
||||
import {
|
||||
type AbsolutePointInputEvent,
|
||||
ActorEvent,
|
||||
END_STATE,
|
||||
type MouseClickEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from '../tool.types';
|
||||
|
||||
/** 선택 객체에서 닫힌 경계를 얻는다. 못 얻으면 안내 후 빈 배열 */
|
||||
function loopFromSelection(input: SequenceInput): Point[] {
|
||||
@@ -29,31 +56,150 @@ function boundingSize(points: Point[]): { width: number; height: number } {
|
||||
return { width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) };
|
||||
}
|
||||
|
||||
export const hatchToolStateMachine = createSequenceTool({
|
||||
tool: Tool.HATCH,
|
||||
helpers: false,
|
||||
steps: [
|
||||
{ kind: 'selection', instructions: '해치를 채울 경계 객체를 선택한 뒤 ENTER를 누르십시오.' },
|
||||
/** 해치 한 벌 — 지금 해치 설정 · 간격 없으면 경계 크기로 */
|
||||
function hatchOf(loop: Point[]) {
|
||||
const { width, height } = boundingSize(loop);
|
||||
return hatchEntity(loop, {
|
||||
style: getHatchStyle(),
|
||||
color: getActiveLineColor(),
|
||||
spacing: getHatchSpacing() ?? autoHatchSpacing(width, height),
|
||||
angle: getHatchAngle(),
|
||||
});
|
||||
}
|
||||
|
||||
const loopArea = (loop: Point[]) =>
|
||||
Math.abs(
|
||||
loop.reduce((sum, point, index) => {
|
||||
const next = loop[(index + 1) % loop.length];
|
||||
return sum + point.x * next.y - next.x * point.y;
|
||||
}, 0)
|
||||
) / 2;
|
||||
|
||||
/** 내부 점 — 그 점을 품은 닫힌 객체(닫힌 폴리선 · 사각형 · 원) 중 가장 작은 하나 */
|
||||
function loopAround(point: Point): { id: string; loop: Point[] } | null {
|
||||
let best: { id: string; loop: Point[]; area: number } | null = null;
|
||||
for (const entity of getPickableEntities()) {
|
||||
const loop = sampleEntityPoints(entity);
|
||||
if (loop.length < 4) continue;
|
||||
const [first, last] = [loop[0], loop[loop.length - 1]];
|
||||
if (Math.hypot(first.x - last.x, first.y - last.y) > 1e-6) continue; // 열린 객체
|
||||
if (!isPointInPolygon(loop, point)) continue;
|
||||
const area = loopArea(loop);
|
||||
if (!best || area < best.area) best = { id: entity.id, loop, area };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
type HatchContext = ToolContext & { boundaries: { id: string; loop: Point[] }[] };
|
||||
type HatchArgs = { context: HatchContext; event: StateEvent };
|
||||
|
||||
const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label });
|
||||
const PICK_PROMPT: PromptSpec = {
|
||||
text: '내부 점 선택',
|
||||
options: [opt('SELECT', 'S', '객체 선택'), OPT.UNDO],
|
||||
absent: [
|
||||
opt('REMOVE', 'B', '경계 제거'),
|
||||
opt('SETTINGS', 'T', '설정'),
|
||||
opt('DRAW', 'W', '그리기'),
|
||||
],
|
||||
commit: (input) => {
|
||||
const loop = loopFromSelection(input);
|
||||
if (!loop.length) return;
|
||||
const { width, height } = boundingSize(loop);
|
||||
addEntities(
|
||||
[
|
||||
hatchEntity(loop, {
|
||||
style: getHatchStyle(),
|
||||
color: getActiveLineColor(),
|
||||
spacing: getHatchSpacing() ?? autoHatchSpacing(width, height),
|
||||
angle: getHatchAngle(),
|
||||
}),
|
||||
],
|
||||
true
|
||||
);
|
||||
setSelectedEntityIds([]);
|
||||
},
|
||||
};
|
||||
const SELECT_CHILD = 'hatchSelect';
|
||||
|
||||
const showBoundaries = (boundaries: HatchContext['boundaries']) =>
|
||||
setGhostHelperEntities(boundaries.flatMap(({ loop }) => polyLineEntity(loop) ?? []));
|
||||
|
||||
/** 내부 점 하나 — 경계 못 찾으면 안내만 · 같은 경계 두 번은 한 번 */
|
||||
const pickAt = assign(({ context, event }: HatchArgs) => {
|
||||
const at =
|
||||
event.type === ActorEvent.MOUSE_CLICK
|
||||
? (event as MouseClickEvent).worldMouseLocation
|
||||
: (event as AbsolutePointInputEvent).value;
|
||||
const found = loopAround(at);
|
||||
if (!found) {
|
||||
toast.warn('닫힌 경계를 찾을 수 없음.');
|
||||
return {};
|
||||
}
|
||||
if (context.boundaries.some(({ id }) => id === found.id)) return {};
|
||||
const boundaries = [...context.boundaries, found];
|
||||
showBoundaries(boundaries);
|
||||
return { boundaries };
|
||||
});
|
||||
|
||||
const undoPick = assign(({ context }: HatchArgs) => {
|
||||
const boundaries = context.boundaries.slice(0, -1);
|
||||
showBoundaries(boundaries);
|
||||
return { boundaries };
|
||||
});
|
||||
|
||||
/** 모은 경계(+ 고른 객체 경계)마다 해치 하나 */
|
||||
const createHatches = ({ context }: { context: HatchContext }) => {
|
||||
const loops = context.boundaries.map(({ loop }) => loop);
|
||||
const selected = getSelectedEntities();
|
||||
if (selected.length) {
|
||||
rememberSelection(selected.map((entity) => entity.id));
|
||||
const loop = entitiesToLoop(selected);
|
||||
if (loop.length >= 3) loops.push(loop);
|
||||
else toast.warn('닫힌 경계를 만들 객체를 선택하십시오.');
|
||||
}
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
if (loops.length) addEntities(loops.map(hatchOf), true);
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 자식 머신을 품은 상태 구성은 xstate 타입 추론이 안 맞음
|
||||
const states: Record<string, any> = {
|
||||
PICK: {
|
||||
meta: { kind: 'point', prompt: PICK_PROMPT },
|
||||
entry: () => setShouldDrawHelpers(false),
|
||||
on: {
|
||||
MOUSE_CLICK: { actions: pickAt },
|
||||
ABSOLUTE_POINT_INPUT: { actions: pickAt },
|
||||
'OPTION.SELECT': { target: 'SELECT' },
|
||||
'OPTION.UNDO': { actions: undoPick },
|
||||
ENTER: { actions: createHatches, target: END_STATE },
|
||||
ESC: { actions: finish, target: END_STATE },
|
||||
},
|
||||
},
|
||||
SELECT: {
|
||||
meta: { kind: 'selection', prompt: SELECT_OBJECTS },
|
||||
invoke: {
|
||||
id: SELECT_CHILD,
|
||||
src: selectToolStateMachine,
|
||||
onDone: { actions: createHatches, target: END_STATE },
|
||||
},
|
||||
on: {
|
||||
MOUSE_CLICK: { actions: sendTo(SELECT_CHILD, ({ event }) => event) },
|
||||
ENTER: { actions: sendTo(SELECT_CHILD, ({ event }) => event) },
|
||||
DRAW: { actions: sendTo(SELECT_CHILD, ({ event }) => event) },
|
||||
'OPTION.*': { actions: sendTo(SELECT_CHILD, ({ event }) => event) },
|
||||
ESC: { actions: finish, target: END_STATE },
|
||||
},
|
||||
},
|
||||
[END_STATE]: { type: 'final' },
|
||||
};
|
||||
|
||||
/**
|
||||
* HATCH — 「내부 점 선택 또는 [객체 선택(S)/명령 취소(U)]:」 되풀이 · 빈 Enter = 만듦 · Esc = 안 만들고 끝.
|
||||
* 손으로 짬 — 순서 엔진은 앞 점을 기준점으로 직교를 걸어 둘째 내부 점이 끌려감.
|
||||
*/
|
||||
export const hatchToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as { context: HatchContext; events: StateEvent },
|
||||
context: { type: Tool.HATCH, boundaries: [] },
|
||||
initial: 'PICK',
|
||||
states,
|
||||
},
|
||||
{
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 자식 머신의 컨텍스트 타입이 달라 그대로 넘긴다
|
||||
actions: { ...(selectToolStateMachine.implementations.actions as any) },
|
||||
}
|
||||
);
|
||||
|
||||
export const gradientToolStateMachine = createSequenceTool({
|
||||
tool: Tool.GRADIENT,
|
||||
helpers: false,
|
||||
|
||||
Reference in New Issue
Block a user