Merge remote-tracking branches 'origin/sub_laptop_4' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
chamferToolStateMachine,
|
||||
filletToolStateMachine,
|
||||
} from '../tools/modify/corner-tools';
|
||||
import { lengthenToolStateMachine } from '../tools/modify/lengthen-tool';
|
||||
import {
|
||||
drawOrderBackToolStateMachine,
|
||||
drawOrderFrontToolStateMachine,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
import { offsetToolStateMachine } from '../tools/modify/offset-tool';
|
||||
import {
|
||||
alignToolStateMachine,
|
||||
lengthenToolStateMachine,
|
||||
mirrorToolStateMachine,
|
||||
stretchToolStateMachine,
|
||||
} from '../tools/modify/transform-tools';
|
||||
|
||||
@@ -51,6 +51,8 @@ export class MeasurementEntity implements Entity {
|
||||
private startPoint: Point;
|
||||
private endPoint: Point;
|
||||
private offsetPoint: Point;
|
||||
/** 치수 문자 바꿈(DIMLINEAR 문자(T)) — `<>` = 잰 값 · 없으면 잰 값 */
|
||||
public textOverride?: string;
|
||||
|
||||
constructor(layerId: string, startPoint: Point, endPoint: Point, offsetPoint: Point) {
|
||||
this.layerId = layerId;
|
||||
@@ -75,6 +77,17 @@ export class MeasurementEntity implements Entity {
|
||||
this.offsetPoint = point;
|
||||
}
|
||||
|
||||
/** 잰 값(치수 소수 자릿수) */
|
||||
public getMeasuredText(): string {
|
||||
return String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals()));
|
||||
}
|
||||
|
||||
/** 보이는 치수 문자 */
|
||||
public getLabel(): string {
|
||||
const measured = this.getMeasuredText();
|
||||
return this.textOverride ? this.textOverride.replaceAll('<>', measured) : measured;
|
||||
}
|
||||
|
||||
public getDrawPoints() {
|
||||
// Return if measurement is zero length
|
||||
if (isPointEqual(this.startPoint, this.endPoint)) {
|
||||
@@ -301,7 +314,7 @@ export class MeasurementEntity implements Entity {
|
||||
drawController.drawLine(offsetStartPointMargin, offsetStartPointExtend);
|
||||
drawController.drawLine(offsetEndPointMargin, offsetEndPointExtend);
|
||||
|
||||
const distance = String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals()));
|
||||
const distance = this.getLabel();
|
||||
const originalTextDirection = normalUnit.rotate90CW();
|
||||
let finalTextDirection = originalTextDirection;
|
||||
if (
|
||||
@@ -343,12 +356,14 @@ export class MeasurementEntity implements Entity {
|
||||
}
|
||||
|
||||
public clone(): MeasurementEntity {
|
||||
return new MeasurementEntity(
|
||||
const copy = new MeasurementEntity(
|
||||
getActiveLayerId(),
|
||||
this.startPoint.clone(),
|
||||
this.endPoint.clone(),
|
||||
this.offsetPoint.clone()
|
||||
);
|
||||
copy.textOverride = this.textOverride;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public intersectsWithBox(box: Box): boolean {
|
||||
@@ -416,7 +431,7 @@ export class MeasurementEntity implements Entity {
|
||||
];
|
||||
|
||||
// Calculate text properties
|
||||
const distance = String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals()));
|
||||
const distance = this.getLabel();
|
||||
const worldFactor = annotationWorldFactor();
|
||||
const textHeight = getDimTextHeight() / worldFactor;
|
||||
// Estimate width: textString.length * fontSize * aspectRatioFactor
|
||||
@@ -582,6 +597,7 @@ export class MeasurementEntity implements Entity {
|
||||
startPoint: { x: this.startPoint.x, y: this.startPoint.y },
|
||||
endPoint: { x: this.endPoint.x, y: this.endPoint.y },
|
||||
offsetPoint: { x: this.offsetPoint.x, y: this.offsetPoint.y },
|
||||
...(this.textOverride ? { textOverride: this.textOverride } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -611,6 +627,7 @@ export class MeasurementEntity implements Entity {
|
||||
measurementEntity.lineColor = jsonEntity.lineColor;
|
||||
measurementEntity.lineWidth = jsonEntity.lineWidth;
|
||||
measurementEntity.lineDash = jsonEntity.lineDash;
|
||||
measurementEntity.textOverride = jsonEntity.shapeData.textOverride;
|
||||
return measurementEntity;
|
||||
}
|
||||
}
|
||||
@@ -619,4 +636,5 @@ export interface MeasurementJsonData {
|
||||
startPoint: { x: number; y: number };
|
||||
endPoint: { x: number; y: number };
|
||||
offsetPoint: { x: number; y: number };
|
||||
textOverride?: string;
|
||||
}
|
||||
|
||||
@@ -379,8 +379,9 @@ export function routeTyped(raw: string): void {
|
||||
return;
|
||||
}
|
||||
if (text.startsWith("'")) return routeTransparent(text.slice(1));
|
||||
const own = subPrompt()?.onText?.(text);
|
||||
if (own) return settle(own);
|
||||
const textSub = subPrompt();
|
||||
const own = textSub?.onText?.(text);
|
||||
if (textSub && own) return settle(textSub, own);
|
||||
if (prompt.kind === 'idle') {
|
||||
// ⑨ 이름 > 내 별칭 > 기본 별칭 딱 맞음만
|
||||
const command = resolveCommandInput(text);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
/** 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절) */
|
||||
/**
|
||||
* 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절).
|
||||
* 오토캐드 물음 차례(337 설계 1-2) — DIMRADIUS · DIMDIAMETER = 「호 또는 원 선택:」 →
|
||||
* 「치수선의 위치 지정 또는 [문자(T)]:」 · 한 번 뒤 끝 · 그 밖 치수 = 물음 글만.
|
||||
*/
|
||||
import { Arc, Circle, Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { OPT, type OptionSpec } from '../../commands/prompt';
|
||||
import { logLine } from '../../commands/run-command';
|
||||
import { getDimDecimals } from '../../commands/dim-settings';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import { MeasurementEntity } from '../../entities/MeasurementEntity';
|
||||
@@ -8,7 +14,7 @@ import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sampl
|
||||
import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { arcEntity, lineEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool';
|
||||
import { annotationText, arrowHead, dimensionText } from './annotation.helpers';
|
||||
|
||||
interface CircularShape {
|
||||
@@ -24,7 +30,24 @@ function circularOf(entity: Entity): CircularShape | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function radialDimension(entity: Entity, textPoint: Point, diameter: boolean): Entity[] | null {
|
||||
/** 잰 값 글(「R10.00」 · 「Ø20.00」) */
|
||||
function radialLabel(entity: Entity, diameter: boolean): string | null {
|
||||
const circular = circularOf(entity);
|
||||
if (!circular) return null;
|
||||
const value = diameter ? circular.radius * 2 : circular.radius;
|
||||
return `${diameter ? 'Ø' : 'R'}${value.toFixed(getDimDecimals())}`;
|
||||
}
|
||||
|
||||
/** 바꾼 글(`<>` = 잰 값) · 없으면 잰 값 */
|
||||
const labelWith = (measured: string, text?: string) =>
|
||||
text && text !== '<>' ? text.replaceAll('<>', measured) : measured;
|
||||
|
||||
function radialDimension(
|
||||
entity: Entity,
|
||||
textPoint: Point,
|
||||
diameter: boolean,
|
||||
text?: string
|
||||
): Entity[] | null {
|
||||
const circular = circularOf(entity);
|
||||
if (!circular) return null;
|
||||
|
||||
@@ -43,52 +66,71 @@ function radialDimension(entity: Entity, textPoint: Point, diameter: boolean): E
|
||||
return [
|
||||
lineEntity(start, textPoint),
|
||||
arrowHead(edge, circular.center),
|
||||
dimensionText(
|
||||
diameter ? circular.radius * 2 : circular.radius,
|
||||
textPoint,
|
||||
diameter ? 'Ø' : 'R'
|
||||
),
|
||||
text && text !== '<>'
|
||||
? annotationText(labelWith(radialLabel(entity, diameter) as string, text), textPoint)
|
||||
: dimensionText(
|
||||
diameter ? circular.radius * 2 : circular.radius,
|
||||
textPoint,
|
||||
diameter ? 'Ø' : 'R'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export const dimRadiusToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMRADIUS,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '반지름을 기입할 원 또는 호를 선택하십시오.' },
|
||||
{ kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' },
|
||||
],
|
||||
commit: (input) => {
|
||||
const parts = radialDimension(input.entity(0), input.point(1), false);
|
||||
if (!parts) {
|
||||
toast.warn('원 또는 호를 선택하십시오.');
|
||||
return;
|
||||
}
|
||||
addEntities(parts, true);
|
||||
},
|
||||
});
|
||||
const MTEXT: OptionSpec = { key: 'MTEXT', kw: 'M', en: 'MTEXT', label: '여러 줄 문자' };
|
||||
const ANGLE: OptionSpec = { key: 'ANGLE', kw: 'A', en: 'ANGLE', label: '각도' };
|
||||
|
||||
export const dimDiameterToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMDIAMETER,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '지름을 기입할 원 또는 호를 선택하십시오.' },
|
||||
{ kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' },
|
||||
],
|
||||
commit: (input) => {
|
||||
const parts = radialDimension(input.entity(0), input.point(1), true);
|
||||
if (!parts) {
|
||||
toast.warn('원 또는 호를 선택하십시오.');
|
||||
return;
|
||||
}
|
||||
addEntities(parts, true);
|
||||
},
|
||||
});
|
||||
/** DIMRADIUS · DIMDIAMETER — 원 · 호 → [문자(T)] → 위치 */
|
||||
function radialTool(tool: Tool, diameter: boolean) {
|
||||
const measured = (input: SequenceInput) =>
|
||||
input.has('obj') ? radialLabel(input.get<Entity>('obj'), diameter) : null;
|
||||
return createSequenceTool({
|
||||
tool,
|
||||
steps: [
|
||||
{ kind: 'entity', id: 'obj', prompt: { text: '호 또는 원 선택' } },
|
||||
{
|
||||
kind: 'text',
|
||||
id: 'txt',
|
||||
when: (input) => input.mode === 'text',
|
||||
next: 'place',
|
||||
defaultValue: '<>',
|
||||
prompt: (input) => {
|
||||
const value = measured(input);
|
||||
return {
|
||||
text: '치수 문자 입력',
|
||||
...(value === null ? {} : { default: { kind: 'enter' as const, label: value } }),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'place',
|
||||
prompt: { text: '치수선의 위치 지정', options: [OPT.TEXT], absent: [MTEXT, ANGLE] },
|
||||
options: { TEXT: { mode: 'text', next: 'txt' } },
|
||||
},
|
||||
],
|
||||
commit: (input) => {
|
||||
const entity = input.get<Entity>('obj');
|
||||
const text = input.has('txt') ? input.get<string>('txt') : undefined;
|
||||
const parts = radialDimension(entity, input.get<Point>('place'), diameter, text);
|
||||
if (!parts) {
|
||||
toast.warn('원 또는 호를 선택하십시오.');
|
||||
return;
|
||||
}
|
||||
addEntities(parts, true);
|
||||
logLine(`치수 문자 = ${labelWith(radialLabel(entity, diameter) as string, text)}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const dimRadiusToolStateMachine = radialTool(Tool.DIMRADIUS, false);
|
||||
export const dimDiameterToolStateMachine = radialTool(Tool.DIMDIAMETER, true);
|
||||
|
||||
export const dimAngularToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMANGULAR,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '각의 꼭짓점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '첫 번째 변 위의 점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '두 번째 변 위의 점을 지정하십시오.' },
|
||||
{ kind: 'point', prompt: { text: '각도 정점 지정' } },
|
||||
{ kind: 'point', prompt: { text: '첫 번째 각도 끝점 지정' } },
|
||||
{ kind: 'point', prompt: { text: '두 번째 각도 끝점 지정' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const [vertex, first, second] = input.points();
|
||||
@@ -128,8 +170,8 @@ export const dimAngularToolStateMachine = createSequenceTool({
|
||||
export const dimArcToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMARC,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '호 길이를 기입할 호를 선택하십시오.' },
|
||||
{ kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' },
|
||||
{ kind: 'entity', prompt: { text: '호 또는 폴리선 호 세그먼트 선택' } },
|
||||
{ kind: 'point', prompt: { text: '호 길이 치수 위치 지정' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const entity = input.entity(0);
|
||||
@@ -146,8 +188,8 @@ export const dimArcToolStateMachine = createSequenceTool({
|
||||
export const dimOrdinateToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMORDINATE,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '좌표를 기입할 피처 위치를 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '지시선 끝점을 지정하십시오.' },
|
||||
{ kind: 'point', prompt: { text: '피쳐 위치 지정' } },
|
||||
{ kind: 'point', prompt: { text: '지시선 끝점 지정' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const [feature, leaderEnd] = input.points();
|
||||
|
||||
@@ -1,27 +1,55 @@
|
||||
/** 선형 치수 계열 — 선형·정렬·기준선·연속·빠른 치수·자동 치수 (조사표 5절) */
|
||||
/**
|
||||
* 선형 치수 계열 — 선형·정렬·기준선·연속·빠른 치수·자동 치수 (조사표 5절).
|
||||
* 오토캐드 물음 차례(337 설계 1-2) — DIMLINEAR · DIMALIGNED = 원점 둘(빈 Enter = 객체 선택) → 치수선 위치
|
||||
* [문자(T)/수평(H)/수직(V)] · 한 번 뒤 끝 · DIMCONTINUE · DIMBASELINE = 원점 되풀이 [명령 취소(U)] · 빈 Enter 끝.
|
||||
*/
|
||||
import { Circle, Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { EntityName } from '../../entities/Entity';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { type OptionSpec, type PromptSpec, OPT, SELECT_OBJECTS } from '../../commands/prompt';
|
||||
import { logLine } from '../../commands/run-command';
|
||||
import { EntityName, type Entity } from '../../entities/Entity';
|
||||
import { MeasurementEntity } from '../../entities/MeasurementEntity';
|
||||
import type { PolyLineEntity } from '../../entities/PolyLineEntity';
|
||||
import { sampleEntityPoints } from '../../helpers/geometry/sample-entity';
|
||||
import { addEntities, getActiveLayerId, setSelectedEntityIds } from '../../state';
|
||||
import { getPointFromEvent } from '../../helpers/get-point-from-event';
|
||||
import { notice } from '../../inputController/prompt-input';
|
||||
import {
|
||||
addEntities,
|
||||
getActiveLayerId,
|
||||
setAngleGuideOriginPoint,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
undo,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { styled } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool';
|
||||
import { END_STATE, type PointInputEvent, type StateEvent, type ToolContext } from '../tool.types';
|
||||
import { getLastDimension, rememberDimension } from './annotation.helpers';
|
||||
|
||||
/** 치수 객체 하나 만들기 — 현재 선 특성을 입히고 직전 치수로 기억한다 */
|
||||
function makeDimension(start: Point, end: Point, offset: Point): MeasurementEntity {
|
||||
function makeDimension(start: Point, end: Point, offset: Point, text?: string): MeasurementEntity {
|
||||
const dimension = styled(new MeasurementEntity(getActiveLayerId(), start, end, offset));
|
||||
if (text && text !== '<>') dimension.textOverride = text;
|
||||
rememberDimension(dimension);
|
||||
return dimension;
|
||||
}
|
||||
|
||||
type Fixed = 'H' | 'V' | null;
|
||||
|
||||
/**
|
||||
* 선형 치수 — 치수선을 놓은 방향으로 수평/수직을 고른다.
|
||||
* 선형 치수 — 수평(H) · 수직(V) 고정이면 그 방향, 아니면 치수선을 놓은 방향으로 고른다.
|
||||
* 위·아래에 놓으면 가로 거리, 좌·우에 놓으면 세로 거리를 잰다 (AutoCAD와 같다).
|
||||
*/
|
||||
function projectForLinear(start: Point, end: Point, offset: Point): [Point, Point] {
|
||||
function projectForLinear(
|
||||
start: Point,
|
||||
end: Point,
|
||||
offset: Point,
|
||||
fixed: Fixed = null
|
||||
): [Point, Point] {
|
||||
if (fixed === 'H') return [start, new Point(end.x, start.y)];
|
||||
if (fixed === 'V') return [start, new Point(start.x, end.y)];
|
||||
const horizontalSpan = Math.abs(end.x - start.x);
|
||||
const verticalSpan = Math.abs(end.y - start.y);
|
||||
const offsetIsVertical =
|
||||
@@ -32,89 +60,268 @@ function projectForLinear(start: Point, end: Point, offset: Point): [Point, Poin
|
||||
return [start, new Point(start.x, end.y)];
|
||||
}
|
||||
|
||||
export const dimLinearToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMLINEAR,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '치수선 위치를 지정하십시오.' },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length !== 2) return [];
|
||||
const [start, end] = projectForLinear(points[0], points[1], input.cursor);
|
||||
return [new MeasurementEntity(getActiveLayerId(), start, end, input.cursor)];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [first, second, offset] = input.points();
|
||||
const [start, end] = projectForLinear(first, second, offset);
|
||||
addEntities([makeDimension(start, end, offset)], true);
|
||||
},
|
||||
});
|
||||
|
||||
export const dimAlignedToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMALIGNED,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '치수선 위치를 지정하십시오.' },
|
||||
],
|
||||
preview: (input) => {
|
||||
const points = input.points();
|
||||
if (points.length !== 2) return [];
|
||||
return [new MeasurementEntity(getActiveLayerId(), points[0], points[1], input.cursor)];
|
||||
},
|
||||
commit: (input) => {
|
||||
const [start, end, offset] = input.points();
|
||||
addEntities([makeDimension(start, end, offset)], true);
|
||||
},
|
||||
});
|
||||
|
||||
export const dimBaselineToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMBASELINE,
|
||||
steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }],
|
||||
commit: (input) => {
|
||||
const previous = getLastDimension();
|
||||
if (!previous) {
|
||||
toast.warn('먼저 치수를 하나 작성하십시오.');
|
||||
return;
|
||||
}
|
||||
const start = previous.getStartPoint();
|
||||
const offset = previous.getOffsetPoint();
|
||||
// 기준선 치수는 같은 시작점에서 재고, 치수선을 한 칸 더 띄운다
|
||||
const spacing = offset.distanceTo(start)[0] * 0.35 || 10;
|
||||
const direction = new Point(offset.x - start.x, offset.y - start.y);
|
||||
const length = Math.hypot(direction.x, direction.y) || 1;
|
||||
const nextOffset = new Point(
|
||||
offset.x + (direction.x / length) * spacing,
|
||||
offset.y + (direction.y / length) * spacing
|
||||
/** 객체 선택 — 선 · 폴리선(누른 토막) · 그 밖은 처음 · 끝 점 */
|
||||
function endsOf(entity: Entity, pick: Point): [Point, Point] | null {
|
||||
let target = entity;
|
||||
if (entity.getType() === EntityName.PolyLine) {
|
||||
const pieces = (entity as PolyLineEntity).getEntities();
|
||||
target = pieces.reduce(
|
||||
(best, piece) =>
|
||||
(piece.getShape()?.distanceTo(pick)[0] ?? Number.POSITIVE_INFINITY) <
|
||||
(best.getShape()?.distanceTo(pick)[0] ?? Number.POSITIVE_INFINITY)
|
||||
? piece
|
||||
: best,
|
||||
pieces[0] ?? entity
|
||||
);
|
||||
addEntities([makeDimension(start, input.point(0), nextOffset)], true);
|
||||
},
|
||||
});
|
||||
}
|
||||
const points = sampleEntityPoints(target);
|
||||
if (points.length < 2) return null;
|
||||
return [points[0], points[points.length - 1]];
|
||||
}
|
||||
|
||||
export const dimContinueToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIMCONTINUE,
|
||||
steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }],
|
||||
commit: (input) => {
|
||||
const previous = getLastDimension();
|
||||
if (!previous) {
|
||||
toast.warn('먼저 치수를 하나 작성하십시오.');
|
||||
return;
|
||||
}
|
||||
addEntities(
|
||||
[makeDimension(previous.getEndPoint(), input.point(0), previous.getOffsetPoint())],
|
||||
true
|
||||
);
|
||||
},
|
||||
const option = (key: string, kw: string, label: string): OptionSpec => ({
|
||||
key,
|
||||
kw,
|
||||
en: key,
|
||||
label,
|
||||
});
|
||||
/** 빈 Enter = 객체 선택 — 숨은 옵션(그림은 <객체 선택>) */
|
||||
const OBJECT = { ...option('OBJECT', 'OBJECT', '객체 선택'), hidden: true as const };
|
||||
const HORIZONTAL = option('HORIZONTAL', 'H', '수평');
|
||||
const VERTICAL = option('VERTICAL', 'V', '수직');
|
||||
const MTEXT = option('MTEXT', 'M', '여러 줄 문자');
|
||||
const ANGLE = option('ANGLE', 'A', '각도');
|
||||
const ROTATED = option('ROTATED', 'R', '회전됨');
|
||||
const SELECT = option('SELECT', 'S', '선택');
|
||||
|
||||
/** 원점 둘 또는 객체 끝 */
|
||||
function originsOf(input: SequenceInput): [Point, Point] | null {
|
||||
if (input.has('obj')) {
|
||||
const entity = input.get<Entity>('obj');
|
||||
return endsOf(entity, input.pick(input.values.indexOf(entity)));
|
||||
}
|
||||
if (!input.has('p2')) return null;
|
||||
return [input.get<Point>('p1'), input.get<Point>('p2')];
|
||||
}
|
||||
|
||||
/** 수평 · 수직 고정 — 명령 하나 동안(확정 · Esc 에서 풂) */
|
||||
let fixed: Fixed = null;
|
||||
|
||||
/** DIMLINEAR · DIMALIGNED 공통 물음 */
|
||||
function linearSteps(tool: Tool, aligned: boolean) {
|
||||
const measure = (input: SequenceInput, offset: Point): [Point, Point] | null => {
|
||||
const origins = originsOf(input);
|
||||
if (!origins) return null;
|
||||
return aligned ? origins : projectForLinear(origins[0], origins[1], offset, fixed);
|
||||
};
|
||||
const measured = (input: SequenceInput): string | null => {
|
||||
const origins = originsOf(input);
|
||||
if (!origins) return null;
|
||||
const [a, b] = origins;
|
||||
const wide = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
|
||||
const [start, end] = aligned ? origins : projectForLinear(a, b, b, fixed ?? (wide ? 'H' : 'V'));
|
||||
return new MeasurementEntity(getActiveLayerId(), start, end, end).getMeasuredText();
|
||||
};
|
||||
return createSequenceTool({
|
||||
tool,
|
||||
steps: [
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'p1',
|
||||
prompt: {
|
||||
text: '첫 번째 치수보조선 원점 지정 또는',
|
||||
options: [OBJECT],
|
||||
default: { kind: 'option', key: 'OBJECT', label: '객체 선택' },
|
||||
},
|
||||
options: { OBJECT: { mode: 'object', next: 'obj' } },
|
||||
},
|
||||
{
|
||||
kind: 'entity',
|
||||
id: 'obj',
|
||||
when: (input) => input.mode === 'object' && !input.has('obj'),
|
||||
next: 'place',
|
||||
prompt: { text: '치수 기입할 객체 선택' },
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'p2',
|
||||
when: (input) => !input.has('obj'),
|
||||
prompt: { text: '두 번째 치수보조선 원점 지정' },
|
||||
},
|
||||
{
|
||||
kind: 'text',
|
||||
id: 'txt',
|
||||
when: (input) => input.mode === 'text',
|
||||
next: 'place',
|
||||
defaultValue: '<>',
|
||||
prompt: (input) => {
|
||||
const value = measured(input);
|
||||
return {
|
||||
text: '치수 문자 입력',
|
||||
...(value === null ? {} : { default: { kind: 'enter' as const, label: value } }),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'place',
|
||||
prompt: {
|
||||
text: '치수선의 위치 지정',
|
||||
options: aligned ? [OPT.TEXT] : [OPT.TEXT, HORIZONTAL, VERTICAL],
|
||||
absent: aligned ? [MTEXT, ANGLE] : [MTEXT, ANGLE, ROTATED],
|
||||
},
|
||||
options: {
|
||||
TEXT: { mode: 'text', next: 'txt' },
|
||||
...(aligned
|
||||
? {}
|
||||
: {
|
||||
HORIZONTAL: {
|
||||
run: () => {
|
||||
fixed = 'H';
|
||||
},
|
||||
},
|
||||
VERTICAL: {
|
||||
run: () => {
|
||||
fixed = 'V';
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
preview: (input) => {
|
||||
if (input.has('place') || !originsOf(input)) return [];
|
||||
const span = measure(input, input.cursor);
|
||||
if (!span) return [];
|
||||
const ghost = new MeasurementEntity(getActiveLayerId(), span[0], span[1], input.cursor);
|
||||
if (input.has('txt')) ghost.textOverride = input.get<string>('txt');
|
||||
return [ghost];
|
||||
},
|
||||
commit: (input) => {
|
||||
const offset = input.get<Point>('place');
|
||||
const span = measure(input, offset);
|
||||
fixed = null;
|
||||
if (!span) {
|
||||
toast.warn('치수를 넣을 수 없는 객체입니다.');
|
||||
return;
|
||||
}
|
||||
const dimension = makeDimension(
|
||||
span[0],
|
||||
span[1],
|
||||
offset,
|
||||
input.has('txt') ? input.get<string>('txt') : undefined
|
||||
);
|
||||
addEntities([dimension], true);
|
||||
logLine(`치수 문자 = ${dimension.getLabel()}`);
|
||||
},
|
||||
onCancel: () => {
|
||||
fixed = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const dimLinearToolStateMachine = linearSteps(Tool.DIMLINEAR, false);
|
||||
export const dimAlignedToolStateMachine = linearSteps(Tool.DIMALIGNED, true);
|
||||
|
||||
/** 기준선 치수는 같은 시작점에서 재고, 치수선을 한 칸 더 띄운다 */
|
||||
function baselineDimension(previous: MeasurementEntity, point: Point): MeasurementEntity {
|
||||
const start = previous.getStartPoint();
|
||||
const offset = previous.getOffsetPoint();
|
||||
const spacing = offset.distanceTo(start)[0] * 0.35 || 10;
|
||||
const direction = new Point(offset.x - start.x, offset.y - start.y);
|
||||
const length = Math.hypot(direction.x, direction.y) || 1;
|
||||
const nextOffset = new Point(
|
||||
offset.x + (direction.x / length) * spacing,
|
||||
offset.y + (direction.y / length) * spacing
|
||||
);
|
||||
return makeDimension(start, point, nextOffset);
|
||||
}
|
||||
|
||||
const continueDimension = (previous: MeasurementEntity, point: Point): MeasurementEntity =>
|
||||
makeDimension(previous.getEndPoint(), point, previous.getOffsetPoint());
|
||||
|
||||
interface ChainContext extends ToolContext {
|
||||
/** 명령 시작 때 직전 치수 · 이 명령이 만든 치수(U 가 뒤에서 뺌) */
|
||||
first: MeasurementEntity | null;
|
||||
made: MeasurementEntity[];
|
||||
}
|
||||
type ChainCtx = { context: ChainContext; event: StateEvent };
|
||||
|
||||
const CHAIN_PROMPT: PromptSpec = {
|
||||
text: '두 번째 치수보조선 원점 지정',
|
||||
options: [OPT.UNDO],
|
||||
absent: [SELECT],
|
||||
};
|
||||
|
||||
/** DIMCONTINUE · DIMBASELINE — 앞 치수에 이어 원점 되풀이 · U = 이 명령 안 마지막 치수 · 빈 Enter · Esc = 끝 */
|
||||
function chainTool(
|
||||
tool: Tool,
|
||||
build: (previous: MeasurementEntity, point: Point) => MeasurementEntity
|
||||
) {
|
||||
const base = (context: ChainContext) => context.made[context.made.length - 1] ?? context.first;
|
||||
const hasBase = ({ context }: ChainCtx) => !!base(context);
|
||||
const place = {
|
||||
guard: hasBase,
|
||||
actions: assign(({ context, event }: ChainCtx) => {
|
||||
const previous = base(context) as MeasurementEntity;
|
||||
const point = getPointFromEvent(previous.getEndPoint(), event as PointInputEvent);
|
||||
const dimension = build(previous, point);
|
||||
addEntities([dimension], true);
|
||||
logLine(`치수 문자 = ${dimension.getLabel()}`);
|
||||
setAngleGuideOriginPoint(dimension.getEndPoint());
|
||||
return { made: [...context.made, dimension] };
|
||||
}),
|
||||
};
|
||||
const noBase = {
|
||||
target: END_STATE,
|
||||
actions: () => notice('먼저 치수를 하나 작성하십시오.'),
|
||||
};
|
||||
return createMachine({
|
||||
types: {} as { context: ChainContext; events: StateEvent },
|
||||
context: () => ({ type: tool, first: getLastDimension(), made: [] }),
|
||||
initial: 'PICK',
|
||||
states: {
|
||||
PICK: {
|
||||
meta: { kind: 'point', prompt: CHAIN_PROMPT },
|
||||
entry: ({ context }: ChainCtx) => {
|
||||
setShouldDrawHelpers(true);
|
||||
setAngleGuideOriginPoint(base(context)?.getEndPoint() ?? null);
|
||||
},
|
||||
on: {
|
||||
MOUSE_CLICK: [place, noBase],
|
||||
ABSOLUTE_POINT_INPUT: [place, noBase],
|
||||
RELATIVE_POINT_INPUT: [place, noBase],
|
||||
NUMBER_INPUT: [place, noBase],
|
||||
'OPTION.UNDO': {
|
||||
actions: assign(({ context }: ChainCtx) => {
|
||||
if (!context.made.length) return {};
|
||||
undo();
|
||||
const made = context.made.slice(0, -1);
|
||||
const previous = made[made.length - 1] ?? context.first;
|
||||
if (previous) rememberDimension(previous);
|
||||
setAngleGuideOriginPoint(previous?.getEndPoint() ?? null);
|
||||
return { made };
|
||||
}),
|
||||
},
|
||||
ENTER: END_STATE,
|
||||
ESC: END_STATE,
|
||||
},
|
||||
},
|
||||
[END_STATE]: { type: 'final' },
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 손으로 짠 상태 — 사건마다 assign 꼴이 달라 풀어 둠(sequence-tool 과 같음)
|
||||
} as Record<string, any>,
|
||||
});
|
||||
}
|
||||
|
||||
export const dimBaselineToolStateMachine = chainTool(Tool.DIMBASELINE, baselineDimension);
|
||||
export const dimContinueToolStateMachine = chainTool(Tool.DIMCONTINUE, continueDimension);
|
||||
|
||||
export const qDimToolStateMachine = createSequenceTool({
|
||||
tool: Tool.QDIM,
|
||||
helpers: false,
|
||||
steps: [
|
||||
{ kind: 'selection', instructions: '치수를 넣을 객체를 선택한 뒤 ENTER.' },
|
||||
{ kind: 'point', instructions: '치수선 위치를 지정하십시오.' },
|
||||
{ kind: 'selection', prompt: { ...SELECT_OBJECTS, text: '치수 기입할 형상 선택' } },
|
||||
{ kind: 'point', prompt: { text: '치수선의 위치 지정' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const offset = input.point(1);
|
||||
@@ -140,8 +347,8 @@ export const dimAutoToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIM,
|
||||
helpers: false,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '치수를 넣을 객체를 선택하십시오.' },
|
||||
{ kind: 'point', instructions: '치수선 위치를 지정하십시오.' },
|
||||
{ kind: 'entity', prompt: { text: '치수 기입할 객체 선택' } },
|
||||
{ kind: 'point', prompt: { text: '치수선의 위치 지정' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const entity = input.entity(0);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/** 등분(DIVIDE)·길이분할(MEASURE) — 객체를 자르지 않고 점만 놓는다 */
|
||||
/**
|
||||
* 등분(DIVIDE)·길이분할(MEASURE) — 객체를 자르지 않고 점만 놓는다.
|
||||
* 오토캐드 물음 차례(337 설계 1-2) · 기본값 없음 · 한 번 뒤 끝 · 블록(B) 없음.
|
||||
*/
|
||||
import { toast } from 'react-toastify';
|
||||
import type { OptionSpec } from '../../commands/prompt';
|
||||
import {
|
||||
dividePoints,
|
||||
measurePoints,
|
||||
@@ -10,12 +14,21 @@ import { Tool } from '../../tools';
|
||||
import { pointEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
|
||||
const BLOCK: OptionSpec = { key: 'BLOCK', kw: 'B', en: 'BLOCK', label: '블록' };
|
||||
|
||||
export const divideToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIVIDE,
|
||||
helpers: false,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '등분할 객체를 선택하십시오.' },
|
||||
{ kind: 'number', instructions: '세그먼트 수를 입력하십시오 <4>.', defaultValue: 4 },
|
||||
{ kind: 'entity', prompt: { text: '등분할 객체 선택' } },
|
||||
{
|
||||
kind: 'number',
|
||||
prompt: {
|
||||
text: '세그먼트 수 입력',
|
||||
absent: [BLOCK],
|
||||
number: { int: true, min: 2, max: 32767 },
|
||||
},
|
||||
},
|
||||
],
|
||||
commit: (input) => {
|
||||
const points = dividePoints(sampleEntityPoints(input.entity(0)), input.number(1));
|
||||
@@ -31,11 +44,19 @@ export const measureLengthToolStateMachine = createSequenceTool({
|
||||
tool: Tool.MEASURE_LENGTH,
|
||||
helpers: false,
|
||||
steps: [
|
||||
{ kind: 'entity', instructions: '길이로 분할할 객체를 선택하십시오.' },
|
||||
{ kind: 'number', instructions: '세그먼트 길이를 입력하십시오 <10>.', defaultValue: 10 },
|
||||
{ kind: 'entity', prompt: { text: '길이 분할 객체 선택' } },
|
||||
{
|
||||
kind: 'number',
|
||||
prompt: { text: '세그먼트의 길이 지정', absent: [BLOCK], number: { positive: true } },
|
||||
},
|
||||
],
|
||||
commit: (input) => {
|
||||
const points = measurePoints(sampleEntityPoints(input.entity(0)), input.number(1));
|
||||
// 누른 자리에 가까운 끝부터 잼(오토캐드)
|
||||
const path = sampleEntityPoints(input.entity(0));
|
||||
const pick = input.pick(0);
|
||||
const nearEnd =
|
||||
path.length > 1 && path[path.length - 1].distanceTo(pick)[0] < path[0].distanceTo(pick)[0];
|
||||
const points = measurePoints(nearEnd ? [...path].reverse() : path, input.number(1));
|
||||
if (!points.length) {
|
||||
toast.warn('지정한 길이로 나눌 수 없습니다.');
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* LENGTHEN(LEN) — 오토캐드 물음 차례(337 설계 1-2).
|
||||
* 「측정할 객체 선택 또는 [증분(DE)/퍼센트(P)/전체(T)] <전체(T)>:」(누름 = 현재 길이 알림)
|
||||
* → 방식 값 → 「변경할 객체 선택 또는 [명령 취소(U)]:」 되풀이(빈 Enter · Esc = 끝 · 바꾼 것 남음).
|
||||
* 손으로 짠 도구(설계 2-14) — 바꾸고 같은 물음에 머무는 되풀이 · U 는 이 명령 안 바꿈만 되돌림.
|
||||
*/
|
||||
import { Arc, type Point, Segment } from '@flatten-js/core';
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { type OptionSpec, type PromptSpec, recall, remember } from '../../commands/prompt';
|
||||
import { ArcEntity } from '../../entities/ArcEntity';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import { findClosestEntity } from '../../helpers/find-closest-entity';
|
||||
import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity';
|
||||
import { pickRadius } from '../../helpers/pick-radius';
|
||||
import { queryEntitiesNearPoint } from '../../helpers/spatial-index';
|
||||
import { notice } from '../../inputController/prompt-input';
|
||||
import { addEntities, deleteEntities, getLayerById, setShouldDrawHelpers, undo } from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import {
|
||||
END_STATE,
|
||||
type MouseClickEvent,
|
||||
type NumberInputEvent,
|
||||
type StateEvent,
|
||||
type ToolContext,
|
||||
} from '../tool.types';
|
||||
import { copyStyle, lengthenLine } from './modify.helpers';
|
||||
|
||||
type Mode = 'DELTA' | 'PERCENT' | 'TOTAL';
|
||||
|
||||
const MODE_OPTIONS: Record<Mode, OptionSpec> = {
|
||||
DELTA: { key: 'DELTA', kw: 'DE', en: 'DELTA', label: '증분' },
|
||||
PERCENT: { key: 'PERCENT', kw: 'P', en: 'PERCENT', label: '퍼센트' },
|
||||
TOTAL: { key: 'TOTAL', kw: 'T', en: 'TOTAL', label: '전체' },
|
||||
};
|
||||
const DYNAMIC: OptionSpec = { key: 'DYNAMIC', kw: 'DY', en: 'DYNAMIC', label: '동적' };
|
||||
const ANGLE: OptionSpec = { key: 'ANGLE', kw: 'A', en: 'ANGLE', label: '각도' };
|
||||
const UNDO: OptionSpec = { key: 'UNDO', kw: 'U', en: 'UNDO', label: '명령 취소' };
|
||||
|
||||
/** 방식별 값 물음 — 기억 열쇠 LENGTHEN.DE · .P · .T */
|
||||
const VALUE_PROMPTS: Record<Mode, PromptSpec> = {
|
||||
DELTA: {
|
||||
text: '증분 길이 입력',
|
||||
absent: [ANGLE],
|
||||
default: { kind: 'value', remember: 'LENGTHEN.DE', initial: 0 },
|
||||
},
|
||||
PERCENT: {
|
||||
text: '퍼센트 길이 입력',
|
||||
number: { positive: true },
|
||||
default: { kind: 'value', remember: 'LENGTHEN.P', initial: 100 },
|
||||
},
|
||||
TOTAL: {
|
||||
text: '전체 길이 지정',
|
||||
absent: [ANGLE],
|
||||
number: { positive: true },
|
||||
default: { kind: 'value', remember: 'LENGTHEN.T', initial: 1 },
|
||||
},
|
||||
};
|
||||
const REMEMBER_KEY: Record<Mode, string> = {
|
||||
DELTA: 'LENGTHEN.DE',
|
||||
PERCENT: 'LENGTHEN.P',
|
||||
TOTAL: 'LENGTHEN.T',
|
||||
};
|
||||
|
||||
const lastMode = (): Mode => recall<Mode>('LENGTHEN.MODE') ?? 'TOTAL';
|
||||
|
||||
const MEASURE_PROMPT = (): PromptSpec => {
|
||||
const mode = MODE_OPTIONS[lastMode()];
|
||||
return {
|
||||
text: '측정할 객체 선택',
|
||||
options: [MODE_OPTIONS.DELTA, MODE_OPTIONS.PERCENT, MODE_OPTIONS.TOTAL],
|
||||
absent: [DYNAMIC],
|
||||
default: { kind: 'option', key: mode.key, label: `${mode.label}(${mode.kw})` },
|
||||
};
|
||||
};
|
||||
const TARGET_PROMPT: PromptSpec = { text: '변경할 객체 선택', options: [UNDO] };
|
||||
|
||||
/** 누른 자리 가장 가까운 객체(잠금 층 뺌 · 반경 안만) */
|
||||
function pickAt(point: Point): Entity | null {
|
||||
const radius = pickRadius();
|
||||
const candidates = queryEntitiesNearPoint(point.x, point.y, radius).filter(
|
||||
(entity) => !getLayerById(entity.layerId)?.isLocked
|
||||
);
|
||||
const { distance, entity } = findClosestEntity(point, candidates);
|
||||
return entity && distance <= radius ? entity : null;
|
||||
}
|
||||
|
||||
export function entityLength(entity: Entity): number {
|
||||
const shape = entity.getShape();
|
||||
if (shape instanceof Segment || shape instanceof Arc) return shape.length;
|
||||
return polylineLength(sampleEntityPoints(entity));
|
||||
}
|
||||
|
||||
/** 호 — 누른 쪽 끝을 호 길이 delta 만큼(사이각 0 ~ 360° 안) */
|
||||
function lengthenArc(entity: Entity, arc: Arc, delta: number, near: Point): Entity | null {
|
||||
const radius = Number(arc.r);
|
||||
const turn = delta / radius;
|
||||
const sweep = arc.sweep + turn;
|
||||
if (sweep <= 0 || sweep >= 2 * Math.PI) return null;
|
||||
const atStart = arc.start.distanceTo(near)[0] < arc.end.distanceTo(near)[0];
|
||||
const sign = arc.counterClockwise ? 1 : -1;
|
||||
const startAngle = atStart ? arc.startAngle - sign * turn : arc.startAngle;
|
||||
const endAngle = atStart ? arc.endAngle : arc.endAngle + sign * turn;
|
||||
return copyStyle(
|
||||
entity,
|
||||
new ArcEntity(entity.layerId, arc.center, radius, startAngle, endAngle, arc.counterClockwise)
|
||||
);
|
||||
}
|
||||
|
||||
/** 방식 · 값 → 바뀐 객체(못 바꾸면 null) */
|
||||
function changed(entity: Entity, mode: Mode, value: number, near: Point): Entity | null {
|
||||
const shape = entity.getShape();
|
||||
if (!(shape instanceof Segment) && !(shape instanceof Arc)) return null;
|
||||
const length = shape.length;
|
||||
const target =
|
||||
mode === 'DELTA' ? length + value : mode === 'PERCENT' ? (length * value) / 100 : value;
|
||||
if (target <= 1e-9) return null;
|
||||
const delta = target - length;
|
||||
return shape instanceof Arc
|
||||
? lengthenArc(entity, shape, delta, near)
|
||||
: lengthenLine(entity, delta, near);
|
||||
}
|
||||
|
||||
interface LengthenContext extends ToolContext {
|
||||
mode: Mode;
|
||||
value: number;
|
||||
/** 이 명령 안에서 바꾼 횟수 — U 는 이만큼만 되돌림 */
|
||||
changes: number;
|
||||
}
|
||||
|
||||
type Ctx = { context: LengthenContext; event: StateEvent };
|
||||
|
||||
const toValue = (mode: Mode) => ({
|
||||
target: `VALUE_${mode}`,
|
||||
actions: assign({ mode: () => mode }),
|
||||
});
|
||||
|
||||
const clickPoint = (event: StateEvent) => (event as MouseClickEvent).worldMouseLocation;
|
||||
|
||||
export const lengthenToolStateMachine = createMachine({
|
||||
types: {} as { context: LengthenContext; events: StateEvent },
|
||||
context: { type: Tool.LENGTHEN, mode: 'TOTAL', value: 1, changes: 0 },
|
||||
initial: 'MEASURE',
|
||||
states: {
|
||||
MEASURE: {
|
||||
meta: { kind: 'entity', prompt: MEASURE_PROMPT },
|
||||
entry: () => setShouldDrawHelpers(false),
|
||||
on: {
|
||||
MOUSE_CLICK: {
|
||||
actions: ({ event }: Ctx) => {
|
||||
const entity = pickAt(clickPoint(event));
|
||||
if (entity) notice(`현재 길이: ${entityLength(entity).toFixed(4)}`);
|
||||
},
|
||||
},
|
||||
'OPTION.DELTA': toValue('DELTA'),
|
||||
'OPTION.PERCENT': toValue('PERCENT'),
|
||||
'OPTION.TOTAL': toValue('TOTAL'),
|
||||
ESC: END_STATE,
|
||||
},
|
||||
},
|
||||
...Object.fromEntries(
|
||||
(Object.keys(VALUE_PROMPTS) as Mode[]).map((mode) => [
|
||||
`VALUE_${mode}`,
|
||||
{
|
||||
meta: { kind: 'number', prompt: VALUE_PROMPTS[mode] },
|
||||
on: {
|
||||
NUMBER_INPUT: {
|
||||
target: 'TARGET',
|
||||
actions: assign(({ event }: Ctx) => {
|
||||
const value = (event as NumberInputEvent).value;
|
||||
remember('LENGTHEN.MODE', mode);
|
||||
remember(REMEMBER_KEY[mode], value);
|
||||
return { value };
|
||||
}),
|
||||
},
|
||||
ESC: END_STATE,
|
||||
},
|
||||
},
|
||||
])
|
||||
),
|
||||
TARGET: {
|
||||
meta: { kind: 'entity', prompt: TARGET_PROMPT },
|
||||
on: {
|
||||
MOUSE_CLICK: {
|
||||
actions: assign(({ context, event }: Ctx) => {
|
||||
const near = clickPoint(event);
|
||||
const entity = pickAt(near);
|
||||
if (!entity) return {};
|
||||
const next = changed(entity, context.mode, context.value, near);
|
||||
if (!next) {
|
||||
notice('이 객체는 길이를 바꿀 수 없음.');
|
||||
return {};
|
||||
}
|
||||
deleteEntities([entity], false);
|
||||
addEntities([next], true);
|
||||
return { changes: context.changes + 1 };
|
||||
}),
|
||||
},
|
||||
'OPTION.UNDO': {
|
||||
actions: assign(({ context }: Ctx) => {
|
||||
if (!context.changes) return {};
|
||||
undo();
|
||||
return { changes: context.changes - 1 };
|
||||
}),
|
||||
},
|
||||
ENTER: END_STATE,
|
||||
ESC: END_STATE,
|
||||
},
|
||||
},
|
||||
[END_STATE]: { type: 'final' },
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 손으로 짠 상태 — 사건마다 assign 꼴이 달라 풀어 둠(sequence-tool 과 같음)
|
||||
} as Record<string, any>,
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
/** 조회 명령 — 거리·반지름·각도·면적·좌표·리스트·계산기 (조사표 3절 유틸리티) */
|
||||
import { Circle, type Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { OptionSpec } from '../../commands/prompt';
|
||||
import { logLine } from '../../commands/run-command';
|
||||
import { EntityName } from '../../entities/Entity';
|
||||
import { entitiesToLoop } from '../../helpers/geometry/entity-loop';
|
||||
import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity';
|
||||
@@ -22,21 +24,30 @@ export function polygonArea(points: Point[]): number {
|
||||
return Math.abs(total) / 2;
|
||||
}
|
||||
|
||||
const MULTIPLE_POINTS: OptionSpec = { key: 'MULTIPLE', kw: 'M', en: 'MULTIPLE', label: '다중 점' };
|
||||
|
||||
/** DIST 결과 두 줄(오토캐드 꼴 · 길이 소수 넷 · 각도 AUPREC 0) */
|
||||
export function distanceReport(first: Point, second: Point): [string, string] {
|
||||
const dx = second.x - first.x;
|
||||
const dy = second.y - first.y;
|
||||
const angle = (toDegrees(Math.atan2(dy, dx)) + 360) % 360;
|
||||
return [
|
||||
`거리 = ${format(Math.hypot(dx, dy), 4)}, XY 평면에서의 각도 = ${Math.round(angle) % 360}, XY 평면으로부터의 각도 = 0`,
|
||||
`X 증분 = ${format(dx, 4)}, Y 증분 = ${format(dy, 4)}, Z 증분 = ${format(0, 4)}`,
|
||||
];
|
||||
}
|
||||
|
||||
export const distanceToolStateMachine = createSequenceTool({
|
||||
tool: Tool.DIST,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '거리를 잴 첫 점을 지정하십시오.' },
|
||||
{ kind: 'point', instructions: '두 번째 점을 지정하십시오.' },
|
||||
{ kind: 'point', prompt: { text: '첫 번째 점 지정' } },
|
||||
{ kind: 'point', prompt: { text: '두 번째 점 지정', absent: [MULTIPLE_POINTS] } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const [first, second] = input.points();
|
||||
const distance = first.distanceTo(second)[0];
|
||||
const angle = toDegrees(Math.atan2(second.y - first.y, second.x - first.x));
|
||||
toast.info(
|
||||
`거리 ${format(distance)} · X 증분 ${format(second.x - first.x)} · Y 증분 ${format(
|
||||
second.y - first.y
|
||||
)} · 각도 ${format(angle, 2)}°`
|
||||
);
|
||||
const lines = distanceReport(first, second);
|
||||
for (const line of lines) logLine(line);
|
||||
toast.info(lines.join('\n'));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* 337 다-2 — 늘이기 · 재기 · 치수 오토캐드 조작(설계 1-2 · 6-3 다 줄 중 LENGTHEN · DIVIDE · MEASURE · DIST · DIM).
|
||||
* 물음 글 · 옵션 글자 · 기본값 · 빈 Enter · Esc · 끝난 뒤를 키 · 마우스로 넣어 숫자로 판정.
|
||||
* 객체는 앱 코드(fromJson)로만 만듦 — 시험 쪽 flatten-js 는 다른 사본.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad measure-acad
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("react-toastify", () => ({
|
||||
toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }),
|
||||
}));
|
||||
|
||||
import { getCommandHistory } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command";
|
||||
import { getDimDecimals } from "../../../B07_DesignDetail/openwebcad/src/commands/dim-settings";
|
||||
import { ArcEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/ArcEntity";
|
||||
import { CircleEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/CircleEntity";
|
||||
import { EntityName, type Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity";
|
||||
import { LineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/LineEntity";
|
||||
import type { MeasurementEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/MeasurementEntity";
|
||||
import { getActiveLayerId, setEntities } from "../../../B07_DesignDetail/openwebcad/src/state";
|
||||
import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools";
|
||||
import {
|
||||
activeTool,
|
||||
click,
|
||||
entities,
|
||||
initIdle,
|
||||
key,
|
||||
noticeLine,
|
||||
promptLine,
|
||||
type,
|
||||
typeEnter,
|
||||
} from "./helpers/cad-drive";
|
||||
|
||||
type Xy = { x: number; y: number };
|
||||
const enter = () => key("Enter");
|
||||
const esc = () => key("Escape");
|
||||
const base = () => ({ lineColor: "#fff", lineWidth: 1, layerId: getActiveLayerId() });
|
||||
|
||||
async function addLine(x1: number, y1: number, x2: number, y2: number): Promise<Entity> {
|
||||
const line = await LineEntity.fromJson({
|
||||
id: `line-${x1}-${y1}-${x2}-${y2}`,
|
||||
type: EntityName.Line,
|
||||
...base(),
|
||||
shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } },
|
||||
} as never);
|
||||
setEntities([...entities(), line], true); // 그린 것처럼 되돌림 기록에 올림
|
||||
return line;
|
||||
}
|
||||
|
||||
async function addArc(): Promise<Entity> {
|
||||
// 중심 (0, 0) · 반지름 50 · 0° → 90° 반시계(길이 25π)
|
||||
const arc = await ArcEntity.fromJson({
|
||||
id: "arc",
|
||||
type: EntityName.Arc,
|
||||
...base(),
|
||||
shapeData: { center: { x: 0, y: 0 }, radius: 50, startAngle: 0, endAngle: Math.PI / 2, counterClockwise: true },
|
||||
} as never);
|
||||
setEntities([...entities(), arc], true); // 그린 것처럼 되돌림 기록에 올림
|
||||
return arc;
|
||||
}
|
||||
|
||||
async function addCircle(): Promise<Entity> {
|
||||
const circle = await CircleEntity.fromJson({
|
||||
id: "circle",
|
||||
type: EntityName.Circle,
|
||||
...base(),
|
||||
shapeData: { center: { x: 300, y: 300 }, radius: 10 },
|
||||
} as never);
|
||||
setEntities([...entities(), circle], true); // 그린 것처럼 되돌림 기록에 올림
|
||||
return circle;
|
||||
}
|
||||
|
||||
const ends = (entity: Entity): [Xy, Xy] => {
|
||||
const shape = entity.getShape() as unknown as { start: Xy; end: Xy };
|
||||
return [
|
||||
{ x: shape.start.x, y: shape.start.y },
|
||||
{ x: shape.end.x, y: shape.end.y },
|
||||
];
|
||||
};
|
||||
const onlyLine = () => entities("Line")[0];
|
||||
const lineLength = () => {
|
||||
const [a, b] = ends(onlyLine());
|
||||
return Math.hypot(b.x - a.x, b.y - a.y);
|
||||
};
|
||||
const points = () =>
|
||||
entities("Point")
|
||||
.map((entity) => {
|
||||
const p = entity.getShape() as unknown as Xy;
|
||||
return { x: Math.round(p.x * 1000) / 1000, y: Math.round(p.y * 1000) / 1000 };
|
||||
})
|
||||
.sort((a, b) => a.x - b.x);
|
||||
const dims = () => entities("Measurement") as MeasurementEntity[];
|
||||
const lastLog = (n = 1) => getCommandHistory().slice(-n);
|
||||
/** 선형 치수 글(반올림 · 끝 0 뗌) · 반지름 치수 글(소수 자릿수 그대로) */
|
||||
const fmt = (value: number) => String(Number(value.toFixed(getDimDecimals())));
|
||||
const fixed = (value: number) => value.toFixed(getDimDecimals());
|
||||
|
||||
beforeEach(() => {
|
||||
initIdle();
|
||||
});
|
||||
|
||||
describe("LENGTHEN(LEN)", () => {
|
||||
const FIRST = "측정할 객체 선택 또는 [증분(DE)/퍼센트(P)/전체(T)]";
|
||||
|
||||
it("누름 = 현재 길이 · DE → 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter = 끝", async () => {
|
||||
await addLine(0, 0, 100, 0);
|
||||
typeEnter("len");
|
||||
expect(activeTool()).toBe(Tool.LENGTHEN);
|
||||
expect(promptLine()).toBe(`${FIRST} <전체(T)>:`);
|
||||
click(50, 0);
|
||||
expect(noticeLine()).toBe("현재 길이: 100.0000");
|
||||
expect(promptLine()).toBe(`${FIRST} <전체(T)>:`);
|
||||
type("d"); // DE 보다 짧음 — 안 맞음
|
||||
expect(noticeLine()).toBe("*선택이 잘못됨*");
|
||||
type("dy");
|
||||
expect(noticeLine()).toBe("「동적(DY)」 옵션은 이 CAD 에 없음.");
|
||||
type("de");
|
||||
expect(promptLine()).toBe("증분 길이 입력 <0.0000>:");
|
||||
type("10");
|
||||
expect(promptLine()).toBe("변경할 객체 선택 또는 [명령 취소(U)]:");
|
||||
click(95, 0); // 끝 쪽
|
||||
expect(ends(onlyLine())[1].x).toBeCloseTo(110);
|
||||
click(105, 0);
|
||||
expect(ends(onlyLine())[1].x).toBeCloseTo(120);
|
||||
expect(activeTool()).toBe(Tool.LENGTHEN); // 되풀이
|
||||
type("u");
|
||||
expect(ends(onlyLine())[1].x).toBeCloseTo(110);
|
||||
type("u");
|
||||
type("u"); // 이 명령 안 바꿈 다 되돌린 뒤 = 아무 일 없음(앞 객체 지키기)
|
||||
expect(entities("Line")).toHaveLength(1);
|
||||
expect(lineLength()).toBeCloseTo(100);
|
||||
click(5, 0); // 시작 쪽
|
||||
expect(ends(onlyLine())[0].x).toBeCloseTo(-10);
|
||||
enter();
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
|
||||
it("마지막 방식 · 값 기억 — 첫 물음 빈 Enter = 그 방식 · 값 빈 Enter = 앞 값", async () => {
|
||||
await addLine(0, 0, 100, 0);
|
||||
typeEnter("len");
|
||||
type("de");
|
||||
type("10");
|
||||
esc();
|
||||
typeEnter("len");
|
||||
expect(promptLine()).toBe(`${FIRST} <증분(DE)>:`);
|
||||
enter();
|
||||
expect(promptLine()).toBe("증분 길이 입력 <10.0000>:");
|
||||
enter();
|
||||
click(95, 0);
|
||||
expect(lineLength()).toBeCloseTo(110);
|
||||
esc(); // Esc = 끝 · 바꾼 것 남음
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
expect(lineLength()).toBeCloseTo(110);
|
||||
});
|
||||
|
||||
it("P · T — 퍼센트 · 전체 · 양수 거절 · 호", async () => {
|
||||
await addLine(0, 0, 100, 0);
|
||||
typeEnter("len");
|
||||
type("p");
|
||||
expect(promptLine()).toBe("퍼센트 길이 입력 <100.0000>:");
|
||||
type("-5");
|
||||
expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함.");
|
||||
type("50");
|
||||
click(90, 0);
|
||||
expect(ends(onlyLine())).toEqual([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 50, y: 0 },
|
||||
]);
|
||||
esc();
|
||||
typeEnter("len");
|
||||
expect(promptLine()).toBe(`${FIRST} <퍼센트(P)>:`);
|
||||
type("t");
|
||||
expect(promptLine()).toBe("전체 길이 지정 <1.0000>:");
|
||||
type("a");
|
||||
expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음.");
|
||||
type("200");
|
||||
click(45, 0);
|
||||
expect(lineLength()).toBeCloseTo(200);
|
||||
await addArc();
|
||||
click(0, 50); // 호 끝(90°) 쪽
|
||||
const arc = entities("Arc")[0].getShape() as unknown as { length: number; endAngle: number };
|
||||
expect(arc.length).toBeCloseTo(200);
|
||||
expect(arc.endAngle).toBeCloseTo(4); // 200 / 50 rad
|
||||
esc();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DIVIDE(DIV) · MEASURE(ME)", () => {
|
||||
it("DIVIDE — 기본 없음 · 정수 2~32767 · 블록 없음 · 한 번 뒤 끝", async () => {
|
||||
await addLine(0, 0, 100, 0);
|
||||
typeEnter("div");
|
||||
expect(promptLine()).toBe("등분할 객체 선택:");
|
||||
click(50, 0);
|
||||
expect(promptLine()).toBe("세그먼트 수 입력:");
|
||||
enter(); // 기본 없음 — 그대로
|
||||
expect(promptLine()).toBe("세그먼트 수 입력:");
|
||||
type("2.5");
|
||||
expect(noticeLine()).toBe("정수 값을 요구함.");
|
||||
type("1");
|
||||
expect(noticeLine()).toBe("2에서 32767 사이의 정수를 요구함.");
|
||||
type("b");
|
||||
expect(noticeLine()).toBe("「블록(B)」 옵션은 이 CAD 에 없음.");
|
||||
type("4");
|
||||
expect(points()).toEqual([
|
||||
{ x: 25, y: 0 },
|
||||
{ x: 50, y: 0 },
|
||||
{ x: 75, y: 0 },
|
||||
]);
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
|
||||
it("MEASURE — 누른 자리 가까운 끝부터 · 양수", async () => {
|
||||
await addLine(0, 0, 100, 0);
|
||||
typeEnter("me");
|
||||
expect(promptLine()).toBe("길이 분할 객체 선택:");
|
||||
click(90, 0); // 끝 쪽
|
||||
expect(promptLine()).toBe("세그먼트의 길이 지정:");
|
||||
type("0");
|
||||
expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함.");
|
||||
type("30");
|
||||
expect(points().map((p) => p.x)).toEqual([10, 40, 70]);
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DIST(DI)", () => {
|
||||
it("두 점 → 오토캐드 꼴 두 줄 기록 · 한 번 뒤 끝 · 다중 점 없음", () => {
|
||||
typeEnter("di");
|
||||
expect(promptLine()).toBe("첫 번째 점 지정:");
|
||||
type("0,0");
|
||||
expect(promptLine()).toBe("두 번째 점 지정:");
|
||||
type("m");
|
||||
expect(noticeLine()).toBe("「다중 점(M)」 옵션은 이 CAD 에 없음.");
|
||||
type("30,40");
|
||||
expect(lastLog(2)).toEqual([
|
||||
"거리 = 50.0000, XY 평면에서의 각도 = 53, XY 평면으로부터의 각도 = 0",
|
||||
"X 증분 = 30.0000, Y 증분 = 40.0000, Z 증분 = 0.0000",
|
||||
]);
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DIMLINEAR(DLI) · DIMALIGNED(DAL)", () => {
|
||||
const PLACE = "치수선의 위치 지정 또는 [문자(T)/수평(H)/수직(V)]:";
|
||||
|
||||
it("원점 둘 → V 고정 → T 문자(`<>` = 잰 값) → 놓음 · 기록 · 한 번 뒤 끝", () => {
|
||||
typeEnter("dli");
|
||||
expect(promptLine()).toBe("첫 번째 치수보조선 원점 지정 또는 <객체 선택>:");
|
||||
type("0,0");
|
||||
expect(promptLine()).toBe("두 번째 치수보조선 원점 지정:");
|
||||
type("100,30");
|
||||
expect(promptLine()).toBe(PLACE);
|
||||
type("m");
|
||||
expect(noticeLine()).toBe("「여러 줄 문자(M)」 옵션은 이 CAD 에 없음.");
|
||||
type("v");
|
||||
expect(promptLine()).toBe(PLACE);
|
||||
type("t");
|
||||
expect(promptLine()).toBe(`치수 문자 입력 <${fmt(30)}>:`);
|
||||
typeEnter("L=<>");
|
||||
expect(promptLine()).toBe(PLACE);
|
||||
type("120,15");
|
||||
const [dim] = dims();
|
||||
expect(dim.getLabel()).toBe(`L=${fmt(30)}`);
|
||||
expect(dim.getEndPoint().x).toBeCloseTo(0); // 수직 — 끝점 x 를 시작점에 맞춤
|
||||
expect(dim.getEndPoint().y).toBeCloseTo(30);
|
||||
expect(lastLog()).toEqual([`치수 문자 = L=${fmt(30)}`]);
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
|
||||
it("첫 물음 빈 Enter = 객체 선택 → H 고정 · T 빈 Enter = 잰 값 그대로", async () => {
|
||||
await addLine(0, 0, 80, 60);
|
||||
typeEnter("dli");
|
||||
enter();
|
||||
expect(promptLine()).toBe("치수 기입할 객체 선택:");
|
||||
click(40, 30);
|
||||
expect(promptLine()).toBe(PLACE);
|
||||
type("h");
|
||||
type("t");
|
||||
expect(promptLine()).toBe(`치수 문자 입력 <${fmt(80)}>:`);
|
||||
enter();
|
||||
type("40,-20"); // 수평 고정이라 옆에 놓아도 가로
|
||||
const [dim] = dims();
|
||||
expect(dim.getLabel()).toBe(fmt(80));
|
||||
expect(dim.textOverride).toBeUndefined();
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
|
||||
it("Esc = 끝 · 안 만듦 · DIMALIGNED 는 [문자(T)] 만", () => {
|
||||
typeEnter("dli");
|
||||
type("0,0");
|
||||
type("10,0");
|
||||
esc();
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
expect(dims()).toHaveLength(0);
|
||||
typeEnter("dal");
|
||||
type("0,0");
|
||||
type("30,40");
|
||||
expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:");
|
||||
type("h");
|
||||
expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함.");
|
||||
type("0,50");
|
||||
expect(dims()[0].getLabel()).toBe(fmt(50));
|
||||
});
|
||||
});
|
||||
|
||||
describe("DIMCONTINUE(DCO) · DIMBASELINE(DBA)", () => {
|
||||
const CHAIN = "두 번째 치수보조선 원점 지정 또는 [명령 취소(U)]:";
|
||||
|
||||
it("되풀이 · U = 이 명령 안 마지막 치수 · 빈 Enter = 끝", () => {
|
||||
typeEnter("dli");
|
||||
type("0,0");
|
||||
type("100,0");
|
||||
type("50,20");
|
||||
expect(dims()).toHaveLength(1);
|
||||
typeEnter("dco");
|
||||
expect(promptLine()).toBe(CHAIN);
|
||||
type("s");
|
||||
expect(noticeLine()).toBe("「선택(S)」 옵션은 이 CAD 에 없음.");
|
||||
type("150,0");
|
||||
type("200,0");
|
||||
expect(dims()).toHaveLength(3);
|
||||
expect(dims()[2].getStartPoint().x).toBeCloseTo(150); // 앞 치수 끝에서 이어짐
|
||||
expect(promptLine()).toBe(CHAIN);
|
||||
type("u");
|
||||
expect(dims()).toHaveLength(2);
|
||||
type("u");
|
||||
type("u"); // 이 명령 밖 치수는 안 지움
|
||||
expect(dims()).toHaveLength(1);
|
||||
type("130,0");
|
||||
expect(dims()[1].getStartPoint().x).toBeCloseTo(100); // U 뒤 앞 치수에서 다시 이음
|
||||
enter();
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
typeEnter("dba");
|
||||
expect(promptLine()).toBe(CHAIN);
|
||||
type("180,0");
|
||||
expect(dims()).toHaveLength(3);
|
||||
expect(dims()[2].getStartPoint().x).toBeCloseTo(100); // 기준선 = 앞 치수 시작점
|
||||
esc();
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DIMRADIUS(DRA) · DIMDIAMETER(DDI)", () => {
|
||||
it("호 또는 원 → [문자(T)] → 위치 · 기록 · 한 번 뒤 끝", async () => {
|
||||
await addCircle();
|
||||
typeEnter("dra");
|
||||
expect(promptLine()).toBe("호 또는 원 선택:");
|
||||
click(310, 300);
|
||||
expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:");
|
||||
type("a");
|
||||
expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음.");
|
||||
type("t");
|
||||
expect(promptLine()).toBe(`치수 문자 입력 <R${fixed(10)}>:`);
|
||||
typeEnter("<> BASE");
|
||||
type("330,300");
|
||||
expect(lastLog()).toEqual([`치수 문자 = R${fixed(10)} BASE`]);
|
||||
expect(activeTool()).toBe(Tool.SELECT);
|
||||
typeEnter("ddi");
|
||||
click(310, 300);
|
||||
type("330,300");
|
||||
expect(lastLog()).toEqual([`치수 문자 = Ø${fixed(20)}`]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user