Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 호(ARC) — 337 설계 1-1 · 오토캐드 차례(순서 엔진 v2).
|
||||
* 「호의 시작점 지정:」 → 「호의 두 번째 점 지정 또는 [중심(C)/끝(E)]:」 → 「호의 끝점 지정:」(세 점) ·
|
||||
* C → 「호의 중심점 지정:」 → 「호의 끝점 지정 또는 [각도(A)]:」(A → 「사이각 지정:」) ·
|
||||
* E → 「호의 끝점 지정:」 → 「호의 중심점 지정 또는 [반지름(R)]:」(R → 「호의 반지름 지정:」) ·
|
||||
* 중심 · 끝 · 반지름 방식은 시작점에서 반시계(오토캐드) · 사이각 − = 시계 · 반지름 = 작은 호. 하나 그리면 끝.
|
||||
*/
|
||||
import { Point } from '@flatten-js/core';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import { type ArcDefinition, arcThroughThreePoints } from '../../helpers/geometry/shape-points';
|
||||
import { addEntities } from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { arcEntity, lineEntity } from '../factories/entity-factory';
|
||||
import { type SequenceInput, createSequenceTool } from '../factories/sequence-tool';
|
||||
import { option, rememberDrawEnd } from './draw-steps';
|
||||
|
||||
const angleOf = (center: Point, point: Point) => Math.atan2(point.y - center.y, point.x - center.x);
|
||||
|
||||
/** 중심 · 시작점 · 끝각 — 반시계(사이각 − 면 시계) */
|
||||
const arcAround = (center: Point, start: Point, endAngle: number, ccw = true): ArcDefinition => ({
|
||||
center,
|
||||
radius: center.distanceTo(start)[0],
|
||||
startAngle: angleOf(center, start),
|
||||
endAngle,
|
||||
counterClockwise: ccw,
|
||||
});
|
||||
|
||||
/** 시작 · 끝 · 반지름 — 반시계 작은 호 · 반지름이 현의 반보다 작으면 null */
|
||||
function arcByRadius(start: Point, end: Point, radius: number): ArcDefinition | null {
|
||||
const half = start.distanceTo(end)[0] / 2;
|
||||
if (half === 0 || radius < half) return null;
|
||||
const rise = Math.sqrt(radius * radius - half * half);
|
||||
const ux = (end.x - start.x) / (2 * half);
|
||||
const uy = (end.y - start.y) / (2 * half);
|
||||
const center = new Point((start.x + end.x) / 2 - uy * rise, (start.y + end.y) / 2 + ux * rise);
|
||||
return arcAround(center, start, angleOf(center, end));
|
||||
}
|
||||
|
||||
/** 지금 값으로 호 — 커서(미리보기)가 마지막 빈 값을 채움 */
|
||||
function arcOf(input: SequenceInput, cursor?: Point): ArcDefinition | null {
|
||||
const start = input.get<Point>('start');
|
||||
const at = (id: string) => (input.has(id) ? input.get<Point>(id) : cursor);
|
||||
if (input.mode === 'C' || input.mode === 'CA') {
|
||||
const center = input.get<Point>('center');
|
||||
if (input.mode === 'CA') {
|
||||
const sweep = input.has('angle') ? input.get<number>('angle') : null;
|
||||
if (sweep === null) return null;
|
||||
return arcAround(center, start, angleOf(center, start) + (sweep * Math.PI) / 180, sweep > 0);
|
||||
}
|
||||
const end = at('cEnd');
|
||||
return end ? arcAround(center, start, angleOf(center, end)) : null;
|
||||
}
|
||||
if (input.mode === 'E' || input.mode === 'ER') {
|
||||
const end = input.get<Point>('eEnd');
|
||||
if (input.mode === 'ER') {
|
||||
const radius = at('radius');
|
||||
return radius ? arcByRadius(start, end, end.distanceTo(radius)[0]) : null;
|
||||
}
|
||||
const center = at('eCenter');
|
||||
return center ? arcAround(center, start, angleOf(center, end)) : null;
|
||||
}
|
||||
const third = at('third');
|
||||
return third ? arcThroughThreePoints(start, input.get<Point>('second'), third) : null;
|
||||
}
|
||||
|
||||
const POINT_TEXT = '호의 끝점 지정';
|
||||
const when =
|
||||
(...modes: (string | null)[]) =>
|
||||
(input: SequenceInput) =>
|
||||
modes.includes(input.mode);
|
||||
|
||||
export const arcToolStateMachine = createSequenceTool({
|
||||
tool: Tool.ARC,
|
||||
steps: [
|
||||
{ kind: 'point', id: 'start', prompt: { text: '호의 시작점 지정' } },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'second',
|
||||
prompt: {
|
||||
text: '호의 두 번째 점 지정',
|
||||
options: [option('CENTER', 'C', '중심'), option('END', 'E', '끝')],
|
||||
},
|
||||
options: { CENTER: { mode: 'C', next: 'center' }, END: { mode: 'E', next: 'eEnd' } },
|
||||
},
|
||||
{ kind: 'point', id: 'third', prompt: { text: POINT_TEXT }, when: when(null) },
|
||||
{ kind: 'point', id: 'center', prompt: { text: '호의 중심점 지정' }, when: when('C', 'CA') },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'cEnd',
|
||||
prompt: { text: POINT_TEXT, options: [option('ANGLE', 'A', '각도')] },
|
||||
options: { ANGLE: { mode: 'CA', next: 'angle' } },
|
||||
when: when('C'),
|
||||
},
|
||||
{ kind: 'number', id: 'angle', prompt: { text: '사이각 지정' }, when: when('CA') },
|
||||
{ kind: 'point', id: 'eEnd', prompt: { text: POINT_TEXT }, when: when('E', 'ER') },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'eCenter',
|
||||
prompt: { text: '호의 중심점 지정', options: [option('RADIUS', 'R', '반지름')] },
|
||||
options: { RADIUS: { mode: 'ER', next: 'radius' } },
|
||||
when: when('E'),
|
||||
},
|
||||
{ kind: 'point', id: 'radius', prompt: { text: '호의 반지름 지정' }, when: when('ER') },
|
||||
],
|
||||
preview: (input) => {
|
||||
const arc = input.values.length > 1 ? arcOf(input, input.cursor) : null;
|
||||
const shown: Entity[] = arc ? [arcEntity(arc)] : [];
|
||||
const last = input.points()[input.points().length - 1];
|
||||
return arc || !last ? shown : [lineEntity(last, input.cursor)];
|
||||
},
|
||||
commit: (input) => {
|
||||
const arc = arcOf(input);
|
||||
if (!arc) {
|
||||
// 세 점 한 줄 = 선(오토캐드는 거절 — 버리지 않게 선으로)
|
||||
if (input.mode === null) {
|
||||
addEntities([lineEntity(input.get<Point>('start'), input.get<Point>('third'))], true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
addEntities([arcEntity(arc)], true);
|
||||
rememberDrawEnd({
|
||||
x: arc.center.x + arc.radius * Math.cos(arc.endAngle),
|
||||
y: arc.center.y + arc.radius * Math.sin(arc.endAngle),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,114 +1,65 @@
|
||||
/** 그리기 명령 — 폴리선·호·다각형·타원·스플라인·점 (조사표 1절) */
|
||||
/** 그리기 명령 — 스플라인·점·도넛 + 폴리선·호·다각형·타원 다시 내보냄 (조사표 1절 · 337 설계 1-1) */
|
||||
import { CircleEntity } from '../../entities/CircleEntity';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import {
|
||||
arcThroughThreePoints,
|
||||
ellipsePoints,
|
||||
regularPolygonPoints,
|
||||
splinePoints,
|
||||
} from '../../helpers/geometry/shape-points';
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import { OPT } from '../../commands/prompt';
|
||||
import { splinePoints } from '../../helpers/geometry/shape-points';
|
||||
import { addEntities, getActiveLayerId } from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import {
|
||||
arcEntity,
|
||||
lineEntity,
|
||||
pointEntity,
|
||||
polyLineEntity,
|
||||
styled,
|
||||
} from '../factories/entity-factory';
|
||||
import { pointEntity, polyLineEntity, styled } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
import { option } from './draw-steps';
|
||||
|
||||
export const plineToolStateMachine = createSequenceTool({
|
||||
tool: Tool.PLINE,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '폴리선의 시작점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' },
|
||||
],
|
||||
repeatLastStep: true,
|
||||
preview: (input) => {
|
||||
const preview = polyLineEntity([...input.points(), input.cursor]);
|
||||
return preview ? [preview] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const polyline = polyLineEntity(input.points());
|
||||
if (polyline) addEntities([polyline], true);
|
||||
},
|
||||
});
|
||||
export { arcToolStateMachine } from './arc-tool';
|
||||
export { ellipseToolStateMachine } from './ellipse-tool';
|
||||
export { plineToolStateMachine } from './pline-tool';
|
||||
export { polygonToolStateMachine } from './polygon-tool';
|
||||
|
||||
export const arcToolStateMachine = createSequenceTool({
|
||||
tool: Tool.ARC,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '호의 시작점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '호가 지나갈 두 번째 점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '호의 끝점을 지정하십시오.' },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length === 1) return [lineEntity(points[0], input.cursor)];
|
||||
if (points.length === 2) {
|
||||
const definition = arcThroughThreePoints(points[0], points[1], input.cursor);
|
||||
return definition ? [arcEntity(definition)] : [lineEntity(points[0], input.cursor)];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [start, through, end] = input.points();
|
||||
const definition = arcThroughThreePoints(start, through, end);
|
||||
// 세 점이 일직선이면 호가 성립하지 않으므로 선으로 대체한다
|
||||
addEntities([definition ? arcEntity(definition) : lineEntity(start, end)], true);
|
||||
},
|
||||
});
|
||||
/** 닫힌 스플라인 — 끝 둘레 이웃을 이어 붙여 그린 뒤 이음 구간만 남김 */
|
||||
function closedSplinePoints(points: Point[]): Point[] {
|
||||
const spans = 12;
|
||||
const wrapped = splinePoints([points[points.length - 1], ...points, points[0], points[1]], spans);
|
||||
const loop = wrapped.slice(spans, spans * (points.length + 1));
|
||||
return [...loop, loop[0]];
|
||||
}
|
||||
|
||||
export const polygonToolStateMachine = createSequenceTool({
|
||||
tool: Tool.POLYGON,
|
||||
steps: [
|
||||
{ kind: 'number', instructions: '면의 수를 입력하십시오 <6>.', defaultValue: 6 },
|
||||
{ kind: 'point', instructions: '다각형의 중심을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '꼭짓점을 지정하십시오 (중심에서의 반지름).' },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length !== 1) return [];
|
||||
const polygon = polyLineEntity(regularPolygonPoints(points[0], input.cursor, input.number(0)));
|
||||
return polygon ? [polygon] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [center, vertex] = input.points();
|
||||
const polygon = polyLineEntity(regularPolygonPoints(center, vertex, input.number(0)));
|
||||
if (polygon) addEntities([polygon], true);
|
||||
},
|
||||
});
|
||||
|
||||
export const ellipseToolStateMachine = createSequenceTool({
|
||||
tool: Tool.ELLIPSE,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '타원의 중심을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '장축의 끝점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '단축 거리를 지정하십시오.' },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length === 1) return [lineEntity(points[0], input.cursor)];
|
||||
if (points.length === 2) {
|
||||
const minor = points[0].distanceTo(input.cursor)[0];
|
||||
const ellipse = polyLineEntity(ellipsePoints(points[0], points[1], minor));
|
||||
return ellipse ? [ellipse] : [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [center, majorPoint, minorPoint] = input.points();
|
||||
const minorRadius = center.distanceTo(minorPoint)[0];
|
||||
const ellipse = polyLineEntity(ellipsePoints(center, majorPoint, minorRadius));
|
||||
if (ellipse) addEntities([ellipse], true);
|
||||
},
|
||||
});
|
||||
const NEXT_POINT = '다음 점 입력';
|
||||
const TOLERANCE = option('TOLERANCE', 'L', '공차');
|
||||
|
||||
/**
|
||||
* 스플라인(SPLINE) — 337 설계 1-1. 「첫 번째 점 지정:」 → 「다음 점 입력:」 → 「다음 점 입력 또는 [명령 취소(U)]:」 →
|
||||
* 넷째 점부터 [명령 취소(U)/닫기(C)] 되풀이 · 빈 Enter = 확정 · C = 닫고 끝 · Esc = 버림.
|
||||
*/
|
||||
export const splineToolStateMachine = createSequenceTool({
|
||||
tool: Tool.SPLINE,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '스플라인의 첫 점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' },
|
||||
{
|
||||
kind: 'point',
|
||||
prompt: {
|
||||
text: '첫 번째 점 지정',
|
||||
absent: [
|
||||
option('METHOD', 'M', '방법'),
|
||||
option('KNOTS', 'K', '매듭'),
|
||||
option('OBJECT', 'O', '객체'),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
prompt: {
|
||||
text: NEXT_POINT,
|
||||
absent: [option('TANGENT', 'T', '시작 접촉부'), TOLERANCE],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
prompt: (input) => ({
|
||||
text: NEXT_POINT,
|
||||
options: input.points().length >= 3 ? [OPT.UNDO, OPT.CLOSE] : [OPT.UNDO],
|
||||
absent: [option('TANGENT', 'T', '끝 접촉부'), TOLERANCE],
|
||||
}),
|
||||
options: { UNDO: 'undo', CLOSE: 'close' },
|
||||
},
|
||||
],
|
||||
repeatLastStep: true,
|
||||
preview: (input) => {
|
||||
@@ -116,7 +67,8 @@ export const splineToolStateMachine = createSequenceTool({
|
||||
return preview ? [preview] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const spline = polyLineEntity(splinePoints(input.points()));
|
||||
const points = input.points();
|
||||
const spline = polyLineEntity(input.closed ? closedSplinePoints(points) : splinePoints(points));
|
||||
if (spline) addEntities([spline], true);
|
||||
},
|
||||
});
|
||||
@@ -138,6 +90,7 @@ export const donutToolStateMachine = createSequenceTool({
|
||||
{ kind: 'number', instructions: '도넛의 외부 지름을 입력하십시오 <2>.', defaultValue: 2 },
|
||||
{ kind: 'point', instructions: '도넛의 중심을 지정하십시오.' },
|
||||
],
|
||||
loopFrom: 2, // 오토캐드 — 중심 되풀이 · 빈 Enter 끝
|
||||
preview: (input) => donutEntities(input.number(0), input.number(1), input.cursor),
|
||||
commit: (input) => {
|
||||
addEntities(donutEntities(input.number(0), input.number(1), input.point(2)), true);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 손으로 짠 그리기 도구 공용(337 설계 2-14) — 점 물음 사건 · 옵션 한 줄 · 끝 상태 · 마지막 그린 끝점(LINE.END).
|
||||
* 선 · 폴리선 · 원 · 사각형 · 다각형 · 타원이 같이 씀.
|
||||
*/
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { type OptionSpec, type XY, recall, remember } from '../../commands/prompt';
|
||||
import { END_STATE } from '../tool.types';
|
||||
|
||||
/** 옵션 하나 — 그림 「label(kw)」 · 사건 `OPTION.<key>` · 맞추기 kw · en */
|
||||
export const option = (key: string, kw: string, label: string, en = key): OptionSpec => ({
|
||||
key,
|
||||
kw,
|
||||
en,
|
||||
label,
|
||||
});
|
||||
|
||||
/** 첫 점 물음(기준점 없음) — 누름 · 절대 좌표만 */
|
||||
export const firstPointEvents = (actions: string | string[], target: string) => ({
|
||||
MOUSE_CLICK: { actions, target },
|
||||
ABSOLUTE_POINT_INPUT: { actions, target },
|
||||
});
|
||||
|
||||
/** 기준점 있는 점 물음 — 누름 · 절대 · 상대 · 거리 숫자 */
|
||||
export const pointEvents = (actions: string | string[], target: string) => ({
|
||||
...firstPointEvents(actions, target),
|
||||
RELATIVE_POINT_INPUT: { actions, target },
|
||||
NUMBER_INPUT: { actions, target },
|
||||
});
|
||||
|
||||
/** 명령 끝 상태 — 들면 쉬는 상태(선택 도구)로 */
|
||||
export const END = { [END_STATE]: { type: 'final' as const } };
|
||||
|
||||
// 마지막 선 · 폴리선 · 호 끝점 — LINE · PLINE 첫 점 빈 Enter 가 여기서 이어 그림(오토캐드)
|
||||
const LINE_END = 'LINE.END';
|
||||
export const rememberDrawEnd = (point: XY): void => remember(LINE_END, { x: point.x, y: point.y });
|
||||
export const lastDrawEnd = (): Point | null => {
|
||||
const point = recall<XY>(LINE_END);
|
||||
return point ? new Point(point.x, point.y) : null;
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 타원(ELLIPSE) — 337 설계 1-1 · 오토캐드 차례.
|
||||
* 「타원의 축 끝점 지정 또는 [중심(C)]:」 → 「축의 다른 끝점 지정:」 → 「다른 축으로 거리를 지정 또는 [회전(R)]:」 ·
|
||||
* C → 「타원의 중심 지정:」 → 「축의 끝점 지정:」 → 같은 거리 물음 · R → 「주축 둘레로 회전 지정:」(다른 축 = 축 × cos).
|
||||
* 다른 축 거리 = 중심에서 잰 반 길이(숫자도 · N10). 하나 그리면 끝.
|
||||
*/
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { getPointFromEvent } from '../../helpers/get-point-from-event';
|
||||
import { ellipsePoints } from '../../helpers/geometry/shape-points';
|
||||
import {
|
||||
addEntities,
|
||||
setAngleGuideOriginPoint,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { lineEntity, polyLineEntity } from '../factories/entity-factory';
|
||||
import {
|
||||
type DrawEvent,
|
||||
END_STATE,
|
||||
type NumberInputEvent,
|
||||
type PointInputEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from '../tool.types';
|
||||
import { END, firstPointEvents, option, pointEvents } from './draw-steps';
|
||||
|
||||
export interface EllipseContext extends ToolContext {
|
||||
/** 축 끝점 방식의 첫 끝 · 중심 방식의 중심 */
|
||||
first: Point | null;
|
||||
center: Point | null;
|
||||
axisEnd: Point | null;
|
||||
}
|
||||
|
||||
enum S {
|
||||
INIT = 'INIT',
|
||||
AXIS_START = 'AXIS_START',
|
||||
AXIS_END = 'AXIS_END',
|
||||
CENTER = 'CENTER',
|
||||
CENTER_AXIS = 'CENTER_AXIS',
|
||||
DISTANCE = 'DISTANCE',
|
||||
ROTATION = 'ROTATION',
|
||||
}
|
||||
|
||||
const ellipseOf = (context: EllipseContext, other: number) =>
|
||||
polyLineEntity(ellipsePoints(context.center as Point, context.axisEnd as Point, other));
|
||||
|
||||
const add = (context: EllipseContext, other: number) => {
|
||||
const ellipse = ellipseOf(context, other);
|
||||
setGhostHelperEntities([]);
|
||||
if (ellipse) addEntities([ellipse], true);
|
||||
};
|
||||
|
||||
/** 축이 정해짐 — 다른 축 거리의 고무줄 · 거리 숫자 기준 = 중심 */
|
||||
const axisDone = (center: Point, axisEnd: Point) => {
|
||||
setAngleGuideOriginPoint(center);
|
||||
return { center, axisEnd };
|
||||
};
|
||||
|
||||
export const ellipseToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as { context: EllipseContext; events: StateEvent },
|
||||
context: { first: null, center: null, axisEnd: null, type: Tool.ELLIPSE },
|
||||
initial: S.INIT,
|
||||
states: {
|
||||
[S.INIT]: { always: { actions: 'init', target: S.AXIS_START } },
|
||||
[S.AXIS_START]: {
|
||||
meta: {
|
||||
kind: 'point',
|
||||
prompt: {
|
||||
text: '타원의 축 끝점 지정',
|
||||
options: [option('CENTER', 'C', '중심')],
|
||||
absent: [option('ARC', 'A', '호')],
|
||||
},
|
||||
},
|
||||
on: {
|
||||
...firstPointEvents('recordFirst', S.AXIS_END),
|
||||
'OPTION.CENTER': { target: S.CENTER },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.AXIS_END]: {
|
||||
meta: { kind: 'point', prompt: { text: '축의 다른 끝점 지정' } },
|
||||
on: {
|
||||
DRAW: { actions: 'previewAxis' },
|
||||
...pointEvents('recordAxisEnd', S.DISTANCE),
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.CENTER]: {
|
||||
meta: { kind: 'point', prompt: { text: '타원의 중심 지정' } },
|
||||
on: { ...firstPointEvents('recordFirst', S.CENTER_AXIS), ESC: { target: END_STATE } },
|
||||
},
|
||||
[S.CENTER_AXIS]: {
|
||||
meta: { kind: 'point', prompt: { text: '축의 끝점 지정' } },
|
||||
on: {
|
||||
DRAW: { actions: 'previewAxis' },
|
||||
...pointEvents('recordCenterAxis', S.DISTANCE),
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.DISTANCE]: {
|
||||
meta: {
|
||||
kind: 'point',
|
||||
prompt: {
|
||||
text: '다른 축으로 거리를 지정',
|
||||
options: [option('ROTATION', 'R', '회전')],
|
||||
number: { positive: true },
|
||||
},
|
||||
},
|
||||
on: {
|
||||
DRAW: { actions: 'previewDistance' },
|
||||
...pointEvents('drawByDistance', END_STATE),
|
||||
'OPTION.ROTATION': { target: S.ROTATION },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.ROTATION]: {
|
||||
meta: {
|
||||
kind: 'number',
|
||||
prompt: { text: '주축 둘레로 회전 지정', number: { min: 0, max: 89.4 } },
|
||||
},
|
||||
on: {
|
||||
NUMBER_INPUT: { actions: 'drawByRotation', target: END_STATE },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
...END,
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
init: () => {
|
||||
setShouldDrawHelpers(true);
|
||||
setSelectedEntityIds([]);
|
||||
setGhostHelperEntities([]);
|
||||
setAngleGuideOriginPoint(null);
|
||||
},
|
||||
recordFirst: assign(({ event }) => {
|
||||
const first = getPointFromEvent(null, event as PointInputEvent);
|
||||
setAngleGuideOriginPoint(first);
|
||||
return { first };
|
||||
}),
|
||||
previewAxis: ({ context, event }) => {
|
||||
const first = context.first as Point;
|
||||
setGhostHelperEntities([lineEntity(first, getPointFromEvent(first, event as DrawEvent))]);
|
||||
},
|
||||
recordAxisEnd: assign(({ context, event }) => {
|
||||
const first = context.first as Point;
|
||||
const end = getPointFromEvent(first, event as PointInputEvent);
|
||||
return axisDone(new Point((first.x + end.x) / 2, (first.y + end.y) / 2), end);
|
||||
}),
|
||||
recordCenterAxis: assign(({ context, event }) => {
|
||||
const center = context.first as Point;
|
||||
return axisDone(center, getPointFromEvent(center, event as PointInputEvent));
|
||||
}),
|
||||
previewDistance: ({ context, event }) => {
|
||||
const center = context.center as Point;
|
||||
const other = center.distanceTo(getPointFromEvent(center, event as DrawEvent))[0];
|
||||
const ellipse = ellipseOf(context, other);
|
||||
setGhostHelperEntities(ellipse ? [ellipse] : []);
|
||||
},
|
||||
drawByDistance: ({ context, event }) => {
|
||||
const center = context.center as Point;
|
||||
add(context, center.distanceTo(getPointFromEvent(center, event as PointInputEvent))[0]);
|
||||
},
|
||||
drawByRotation: ({ context, event }) => {
|
||||
const axis = (context.center as Point).distanceTo(context.axisEnd as Point)[0];
|
||||
const angle = ((event as NumberInputEvent).value * Math.PI) / 180;
|
||||
add(context, axis * Math.cos(angle));
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 폴리선(PLINE) — 337 설계 1-1 · 오토캐드 차례.
|
||||
* 「시작점 지정:」(빈 Enter = 마지막 끝점에서) → 「다음 점 지정 또는 [호(A)/길이(L)/명령 취소(U)]:」 되풀이 ·
|
||||
* 둘째 토막부터 [호(A)/닫기(C)/길이(L)/명령 취소(U)] · 호 모드 「호의 끝점 지정 또는 [닫기(CL)/선(L)/명령 취소(U)]:」.
|
||||
* 호 = 앞 토막 끝 방향에 접하는 호(첫 토막이면 동쪽 0°) · 길이 = 앞 토막 방향으로 그 길이 ·
|
||||
* 빈 Enter · Esc = 그린 데까지 폴리선 남기고 끝.
|
||||
*/
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { OPT, type PromptSpec, type XY } from '../../commands/prompt';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import { PolyLineEntity } from '../../entities/PolyLineEntity';
|
||||
import { getPointFromEvent } from '../../helpers/get-point-from-event';
|
||||
import type { ArcDefinition } from '../../helpers/geometry/shape-points';
|
||||
import {
|
||||
addEntities,
|
||||
getActiveLayerId,
|
||||
setAngleGuideOriginPoint,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { arcEntity, lineEntity, styled } from '../factories/entity-factory';
|
||||
import {
|
||||
type DrawEvent,
|
||||
END_STATE,
|
||||
type NumberInputEvent,
|
||||
type PointInputEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from '../tool.types';
|
||||
import {
|
||||
END,
|
||||
firstPointEvents,
|
||||
lastDrawEnd,
|
||||
option,
|
||||
pointEvents,
|
||||
rememberDrawEnd,
|
||||
} from './draw-steps';
|
||||
|
||||
/** 토막 하나 — 끝점 · 호면 그 정의(없으면 선) */
|
||||
interface Segment {
|
||||
end: Point;
|
||||
arc: ArcDefinition | null;
|
||||
}
|
||||
|
||||
export interface PlineContext extends ToolContext {
|
||||
start: Point | null;
|
||||
segments: Segment[];
|
||||
}
|
||||
|
||||
enum S {
|
||||
INIT = 'INIT',
|
||||
START = 'START',
|
||||
LINE = 'LINE',
|
||||
ARC = 'ARC',
|
||||
LENGTH = 'LENGTH',
|
||||
}
|
||||
|
||||
const EAST: XY = { x: 1, y: 0 };
|
||||
|
||||
const lastEnd = (context: PlineContext): Point =>
|
||||
context.segments[context.segments.length - 1]?.end ?? (context.start as Point);
|
||||
|
||||
/** 마지막 토막 끝의 진행 방향(단위) — 선 = 그 방향 · 호 = 끝 접선 · 토막 없음 = 동쪽 */
|
||||
function endDirection(context: PlineContext): XY {
|
||||
const count = context.segments.length;
|
||||
if (!count) return EAST;
|
||||
const { end, arc } = context.segments[count - 1];
|
||||
if (arc) {
|
||||
const rx = (end.x - arc.center.x) / arc.radius;
|
||||
const ry = (end.y - arc.center.y) / arc.radius;
|
||||
return arc.counterClockwise ? { x: -ry, y: rx } : { x: ry, y: -rx };
|
||||
}
|
||||
const from = count > 1 ? context.segments[count - 2].end : (context.start as Point);
|
||||
const length = Math.hypot(end.x - from.x, end.y - from.y);
|
||||
return length > 0 ? { x: (end.x - from.x) / length, y: (end.y - from.y) / length } : EAST;
|
||||
}
|
||||
|
||||
/** from 에서 방향 dir 에 접하고 to 를 지나는 호 — 한 줄 위면 null(선) */
|
||||
export function tangentArc(from: Point, dir: XY, to: Point): ArcDefinition | null {
|
||||
const dx = to.x - from.x;
|
||||
const dy = to.y - from.y;
|
||||
const cross = dir.x * dy - dir.y * dx;
|
||||
if (Math.abs(cross) < 1e-9) return null;
|
||||
const signed = (dx * dx + dy * dy) / (2 * cross); // + = 왼쪽 중심(반시계)
|
||||
const center = new Point(from.x - dir.y * signed, from.y + dir.x * signed);
|
||||
return {
|
||||
center,
|
||||
radius: Math.abs(signed),
|
||||
startAngle: Math.atan2(from.y - center.y, from.x - center.x),
|
||||
endAngle: Math.atan2(to.y - center.y, to.x - center.x),
|
||||
counterClockwise: cross > 0,
|
||||
};
|
||||
}
|
||||
|
||||
const segmentEntity = (from: Point, segment: Segment): Entity =>
|
||||
segment.arc ? arcEntity(segment.arc) : lineEntity(from, segment.end);
|
||||
|
||||
function segmentEntities(context: PlineContext, extra?: Segment): Entity[] {
|
||||
const list = extra ? [...context.segments, extra] : context.segments;
|
||||
let from = context.start as Point;
|
||||
return list.map((segment) => {
|
||||
const entity = segmentEntity(from, segment);
|
||||
from = segment.end;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
const lineTo = (end: Point): Segment => ({ end, arc: null });
|
||||
const arcTo = (context: PlineContext, end: Point): Segment => ({
|
||||
end,
|
||||
arc: tangentArc(lastEnd(context), endDirection(context), end),
|
||||
});
|
||||
|
||||
const ARC = option('ARC', 'A', '호');
|
||||
const LENGTH = option('LENGTH', 'L', '길이');
|
||||
const LINE = option('LINE', 'L', '선');
|
||||
const ARC_CLOSE = option('CLOSE', 'CL', '닫기');
|
||||
const WIDTHS = [option('HALFWIDTH', 'H', '반폭'), option('WIDTH', 'W', '폭')];
|
||||
const hasSegment = (context: Partial<PlineContext>) => (context.segments?.length ?? 0) > 0;
|
||||
|
||||
const linePrompt = (context: Partial<PlineContext>): PromptSpec => ({
|
||||
text: '다음 점 지정',
|
||||
options: hasSegment(context) ? [ARC, OPT.CLOSE, LENGTH, OPT.UNDO] : [ARC, LENGTH, OPT.UNDO],
|
||||
absent: WIDTHS,
|
||||
});
|
||||
const arcPrompt = (context: Partial<PlineContext>): PromptSpec => ({
|
||||
text: '호의 끝점 지정',
|
||||
options: hasSegment(context) ? [ARC_CLOSE, LINE, OPT.UNDO] : [LINE, OPT.UNDO],
|
||||
absent: [
|
||||
option('ANGLE', 'A', '각도'),
|
||||
option('CENTER', 'CE', '중심'),
|
||||
option('DIRECTION', 'D', '방향'),
|
||||
option('RADIUS', 'R', '반지름'),
|
||||
option('SECOND', 'S', '두 번째 점'),
|
||||
...WIDTHS,
|
||||
],
|
||||
});
|
||||
|
||||
/** 토막 하나 더함 — 기준점 · 끝점 기억 */
|
||||
const pushSegment = (context: PlineContext, segment: Segment) => {
|
||||
setGhostHelperEntities([]);
|
||||
setAngleGuideOriginPoint(segment.end);
|
||||
return { segments: [...context.segments, segment] };
|
||||
};
|
||||
|
||||
export const plineToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as { context: PlineContext; events: StateEvent },
|
||||
context: { start: null, segments: [], type: Tool.PLINE },
|
||||
initial: S.INIT,
|
||||
states: {
|
||||
[S.INIT]: { always: { actions: 'init', target: S.START } },
|
||||
[S.START]: {
|
||||
meta: { kind: 'point', prompt: { text: '시작점 지정' } },
|
||||
on: {
|
||||
...firstPointEvents('recordStart', S.LINE),
|
||||
ENTER: { guard: () => !!lastDrawEnd(), actions: 'continueFromLast', target: S.LINE },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.LINE]: {
|
||||
meta: { kind: 'point', prompt: linePrompt },
|
||||
on: {
|
||||
DRAW: { actions: 'previewLine' },
|
||||
...pointEvents('addLine', S.LINE),
|
||||
'OPTION.ARC': { target: S.ARC },
|
||||
'OPTION.LENGTH': { target: S.LENGTH },
|
||||
'OPTION.CLOSE': {
|
||||
guard: 'hasSegment',
|
||||
actions: ['closeLine', 'commit'],
|
||||
target: END_STATE,
|
||||
},
|
||||
'OPTION.UNDO': { actions: 'undoSegment' },
|
||||
ENTER: { actions: 'commit', target: END_STATE },
|
||||
ESC: { actions: 'commit', target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.ARC]: {
|
||||
meta: { kind: 'point', prompt: arcPrompt },
|
||||
on: {
|
||||
DRAW: { actions: 'previewArc' },
|
||||
...pointEvents('addArc', S.ARC),
|
||||
'OPTION.LINE': { target: S.LINE },
|
||||
'OPTION.CLOSE': {
|
||||
guard: 'hasSegment',
|
||||
actions: ['closeArc', 'commit'],
|
||||
target: END_STATE,
|
||||
},
|
||||
'OPTION.UNDO': { actions: 'undoSegment' },
|
||||
ENTER: { actions: 'commit', target: END_STATE },
|
||||
ESC: { actions: 'commit', target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.LENGTH]: {
|
||||
meta: { kind: 'number', prompt: { text: '선의 길이 지정', number: { positive: true } } },
|
||||
on: {
|
||||
NUMBER_INPUT: { actions: 'addLength', target: S.LINE },
|
||||
ESC: { actions: 'commit', target: END_STATE },
|
||||
},
|
||||
},
|
||||
...END,
|
||||
},
|
||||
},
|
||||
{
|
||||
// 옵션 사건은 이름 붙인 guard 로(줄 안 guard 는 글자 사건 꼴과 타입이 안 맞음)
|
||||
guards: { hasSegment: ({ context }) => hasSegment(context) },
|
||||
actions: {
|
||||
init: assign(() => {
|
||||
setShouldDrawHelpers(true);
|
||||
setSelectedEntityIds([]);
|
||||
setGhostHelperEntities([]);
|
||||
setAngleGuideOriginPoint(null);
|
||||
return { start: null, segments: [] };
|
||||
}),
|
||||
recordStart: assign(({ event }) => {
|
||||
const start = getPointFromEvent(null, event as PointInputEvent);
|
||||
setAngleGuideOriginPoint(start);
|
||||
return { start };
|
||||
}),
|
||||
continueFromLast: assign(() => {
|
||||
const start = lastDrawEnd() as Point;
|
||||
setAngleGuideOriginPoint(start);
|
||||
return { start };
|
||||
}),
|
||||
previewLine: ({ context, event }) => {
|
||||
const end = getPointFromEvent(lastEnd(context), event as DrawEvent);
|
||||
setGhostHelperEntities(segmentEntities(context, lineTo(end)));
|
||||
},
|
||||
previewArc: ({ context, event }) => {
|
||||
const end = getPointFromEvent(lastEnd(context), event as DrawEvent);
|
||||
setGhostHelperEntities(segmentEntities(context, arcTo(context, end)));
|
||||
},
|
||||
addLine: assign(({ context, event }) =>
|
||||
pushSegment(context, lineTo(getPointFromEvent(lastEnd(context), event as PointInputEvent)))
|
||||
),
|
||||
addArc: assign(({ context, event }) =>
|
||||
pushSegment(
|
||||
context,
|
||||
arcTo(context, getPointFromEvent(lastEnd(context), event as PointInputEvent))
|
||||
)
|
||||
),
|
||||
addLength: assign(({ context, event }) => {
|
||||
const from = lastEnd(context);
|
||||
const dir = endDirection(context);
|
||||
const length = (event as NumberInputEvent).value;
|
||||
return pushSegment(
|
||||
context,
|
||||
lineTo(new Point(from.x + dir.x * length, from.y + dir.y * length))
|
||||
);
|
||||
}),
|
||||
closeLine: assign(({ context }) => ({
|
||||
segments: [...context.segments, lineTo(context.start as Point)],
|
||||
})),
|
||||
closeArc: assign(({ context }) => ({
|
||||
segments: [...context.segments, arcTo(context, context.start as Point)],
|
||||
})),
|
||||
undoSegment: assign(({ context }) => {
|
||||
const segments = context.segments.slice(0, -1);
|
||||
setGhostHelperEntities([]);
|
||||
setAngleGuideOriginPoint(segments[segments.length - 1]?.end ?? context.start);
|
||||
return { segments };
|
||||
}),
|
||||
commit: ({ context }) => {
|
||||
setGhostHelperEntities([]);
|
||||
if (!context.segments.length) return;
|
||||
addEntities(
|
||||
[styled(new PolyLineEntity(getActiveLayerId(), segmentEntities(context)))],
|
||||
true
|
||||
);
|
||||
rememberDrawEnd(lastEnd(context));
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 폴리곤(POLYGON) — 337 설계 1-1 · 오토캐드 차례.
|
||||
* 「변의 수 입력 <4>:」(3~1024 정수 · 기억) → 「폴리곤의 중심 지정 또는 [모서리(E)]:」 →
|
||||
* 「옵션 입력 [원에 내접(I)/원에 외접(C)] <I>:」(기억) → 「원의 반지름 지정:」 ·
|
||||
* E → 「모서리의 첫 번째 끝점 지정:」 → 「모서리의 두 번째 끝점 지정:」(그 모서리에서 반시계).
|
||||
* 반지름 숫자 = 아래 변 수평 · 점 = 크기 + 돌림(내접 = 꼭짓점 · 외접 = 변 가운데). 하나 그리면 끝.
|
||||
*/
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { type PromptSpec, recall, remember } from '../../commands/prompt';
|
||||
import { getPointFromEvent } from '../../helpers/get-point-from-event';
|
||||
import { regularPolygonPoints } from '../../helpers/geometry/shape-points';
|
||||
import {
|
||||
addEntities,
|
||||
setAngleGuideOriginPoint,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { polyLineEntity } from '../factories/entity-factory';
|
||||
import {
|
||||
type DrawEvent,
|
||||
END_STATE,
|
||||
type NumberInputEvent,
|
||||
type PointInputEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from '../tool.types';
|
||||
import { END, firstPointEvents, option, pointEvents } from './draw-steps';
|
||||
|
||||
export interface PolygonContext extends ToolContext {
|
||||
sides: number;
|
||||
center: Point | null;
|
||||
edgeStart: Point | null;
|
||||
}
|
||||
|
||||
enum S {
|
||||
INIT = 'INIT',
|
||||
SIDES = 'SIDES',
|
||||
CENTER = 'CENTER',
|
||||
FIT = 'FIT',
|
||||
RADIUS = 'RADIUS',
|
||||
EDGE_START = 'EDGE_START',
|
||||
EDGE_END = 'EDGE_END',
|
||||
}
|
||||
|
||||
const SIDES_KEY = 'POLYSIDES';
|
||||
const FIT_KEY = 'POLYGON.IC';
|
||||
const INSCRIBED = option('INSCRIBED', 'I', '원에 내접');
|
||||
const CIRCUMSCRIBED = option('CIRCUMSCRIBED', 'C', '원에 외접');
|
||||
const circumscribed = () => recall(FIT_KEY) === CIRCUMSCRIBED.key;
|
||||
|
||||
const SIDES_PROMPT: PromptSpec = {
|
||||
text: '변의 수 입력',
|
||||
default: { kind: 'value', remember: SIDES_KEY, initial: 4, format: 'int' },
|
||||
number: { int: true, min: 3, max: 1024 },
|
||||
};
|
||||
const fitPrompt = (): PromptSpec => {
|
||||
const fit = circumscribed() ? CIRCUMSCRIBED : INSCRIBED;
|
||||
return {
|
||||
text: '옵션 입력',
|
||||
options: [INSCRIBED, CIRCUMSCRIBED],
|
||||
default: { kind: 'option', key: fit.key, label: fit.kw },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 첫 꼭짓점 — 내접 = 원 위 꼭짓점 · 외접 = 변 가운데가 원 위(꼭짓점은 반 칸 돌아 R / cos(π/n)).
|
||||
* 각 null = 아래 변 수평.
|
||||
*/
|
||||
function firstVertex(center: Point, radius: number, angle: number | null, sides: number): Point {
|
||||
const half = Math.PI / sides;
|
||||
const outer = circumscribed();
|
||||
const toward = angle ?? -Math.PI / 2 + (outer ? 0 : half); // 아래 변 가운데(외접) · 그 오른쪽 꼭짓점(내접)
|
||||
const at = outer ? toward - half : toward;
|
||||
const reach = outer ? radius / Math.cos(half) : radius;
|
||||
return new Point(center.x + reach * Math.cos(at), center.y + reach * Math.sin(at));
|
||||
}
|
||||
|
||||
function polygonFrom(center: Point, point: Point | null, radius: number, sides: number) {
|
||||
const angle = point ? Math.atan2(point.y - center.y, point.x - center.x) : null;
|
||||
return polyLineEntity(
|
||||
regularPolygonPoints(center, firstVertex(center, radius, angle, sides), sides)
|
||||
);
|
||||
}
|
||||
|
||||
/** 모서리 a → b 에서 반시계로 — 중심은 모서리 왼쪽 */
|
||||
function polygonOnEdge(a: Point, b: Point, sides: number) {
|
||||
const length = a.distanceTo(b)[0];
|
||||
if (!length) return null;
|
||||
const apothem = length / 2 / Math.tan(Math.PI / sides);
|
||||
const center = new Point(
|
||||
(a.x + b.x) / 2 - ((b.y - a.y) / length) * apothem,
|
||||
(a.y + b.y) / 2 + ((b.x - a.x) / length) * apothem
|
||||
);
|
||||
return polyLineEntity(regularPolygonPoints(center, a, sides));
|
||||
}
|
||||
|
||||
const add = (entity: ReturnType<typeof polyLineEntity>) => {
|
||||
setGhostHelperEntities([]);
|
||||
if (entity) addEntities([entity], true);
|
||||
};
|
||||
|
||||
export const polygonToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as { context: PolygonContext; events: StateEvent },
|
||||
context: { sides: 4, center: null, edgeStart: null, type: Tool.POLYGON },
|
||||
initial: S.INIT,
|
||||
states: {
|
||||
[S.INIT]: { always: { actions: 'init', target: S.SIDES } },
|
||||
[S.SIDES]: {
|
||||
meta: { kind: 'number', prompt: SIDES_PROMPT },
|
||||
on: {
|
||||
NUMBER_INPUT: { actions: 'recordSides', target: S.CENTER },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.CENTER]: {
|
||||
meta: {
|
||||
kind: 'point',
|
||||
prompt: { text: '폴리곤의 중심 지정', options: [option('EDGE', 'E', '모서리')] },
|
||||
},
|
||||
on: {
|
||||
...firstPointEvents('recordCenter', S.FIT),
|
||||
'OPTION.EDGE': { target: S.EDGE_START },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.FIT]: {
|
||||
meta: { kind: 'keyword', prompt: fitPrompt },
|
||||
on: {
|
||||
'OPTION.INSCRIBED': { actions: 'recordFit', target: S.RADIUS },
|
||||
'OPTION.CIRCUMSCRIBED': { actions: 'recordFit', target: S.RADIUS },
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.RADIUS]: {
|
||||
meta: {
|
||||
kind: 'point',
|
||||
prompt: { text: '원의 반지름 지정', number: { positive: true } },
|
||||
},
|
||||
on: {
|
||||
DRAW: { actions: 'previewRadius' },
|
||||
...pointEvents('drawByRadius', END_STATE),
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
[S.EDGE_START]: {
|
||||
meta: { kind: 'point', prompt: { text: '모서리의 첫 번째 끝점 지정' } },
|
||||
on: { ...firstPointEvents('recordEdge', S.EDGE_END), ESC: { target: END_STATE } },
|
||||
},
|
||||
[S.EDGE_END]: {
|
||||
meta: { kind: 'point', prompt: { text: '모서리의 두 번째 끝점 지정' } },
|
||||
on: {
|
||||
DRAW: { actions: 'previewEdge' },
|
||||
...pointEvents('drawByEdge', END_STATE),
|
||||
ESC: { target: END_STATE },
|
||||
},
|
||||
},
|
||||
...END,
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
init: () => {
|
||||
setShouldDrawHelpers(true);
|
||||
setSelectedEntityIds([]);
|
||||
setGhostHelperEntities([]);
|
||||
setAngleGuideOriginPoint(null);
|
||||
},
|
||||
recordSides: assign(({ event }) => {
|
||||
const sides = (event as NumberInputEvent).value;
|
||||
remember(SIDES_KEY, sides);
|
||||
return { sides };
|
||||
}),
|
||||
recordCenter: assign(({ event }) => {
|
||||
const center = getPointFromEvent(null, event as PointInputEvent);
|
||||
setAngleGuideOriginPoint(center); // 반지름 물음의 고무줄 · 거리 숫자 기준
|
||||
return { center };
|
||||
}),
|
||||
recordFit: ({ event }) => remember(FIT_KEY, event.type.slice('OPTION.'.length)),
|
||||
previewRadius: ({ context, event }) => {
|
||||
const center = context.center as Point;
|
||||
const point = getPointFromEvent(center, event as DrawEvent);
|
||||
const polygon = polygonFrom(center, point, center.distanceTo(point)[0], context.sides);
|
||||
setGhostHelperEntities(polygon ? [polygon] : []);
|
||||
},
|
||||
drawByRadius: ({ context, event }) => {
|
||||
const center = context.center as Point;
|
||||
if (event.type === 'NUMBER_INPUT') {
|
||||
add(polygonFrom(center, null, (event as NumberInputEvent).value, context.sides));
|
||||
return;
|
||||
}
|
||||
const point = getPointFromEvent(center, event as PointInputEvent);
|
||||
add(polygonFrom(center, point, center.distanceTo(point)[0], context.sides));
|
||||
},
|
||||
recordEdge: assign(({ event }) => {
|
||||
const edgeStart = getPointFromEvent(null, event as PointInputEvent);
|
||||
setAngleGuideOriginPoint(edgeStart);
|
||||
return { edgeStart };
|
||||
}),
|
||||
previewEdge: ({ context, event }) => {
|
||||
const start = context.edgeStart as Point;
|
||||
const polygon = polygonOnEdge(
|
||||
start,
|
||||
getPointFromEvent(start, event as DrawEvent),
|
||||
context.sides
|
||||
);
|
||||
setGhostHelperEntities(polygon ? [polygon] : []);
|
||||
},
|
||||
drawByEdge: ({ context, event }) => {
|
||||
const start = context.edgeStart as Point;
|
||||
add(
|
||||
polygonOnEdge(start, getPointFromEvent(start, event as PointInputEvent), context.sides)
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user