feat(B07): 그립으로 고치고, 음수·극좌표를 받고, 편집분을 잃지 않는다
조사표 8~13절 검토에서 "기본 기능"으로 고른 것을 반영한다. - 좌표 입력: 절대·상대 좌표가 양수만 받아 `@-100,50`을 거부했다. 부호를 허용하고 극좌표 `@거리<각도`·`거리<각도`를 더했다. `-`·`+`가 확대·축소 단축키로 먼저 잡혀 음수의 첫 글자를 먹고 있어 그 두 단축키를 뺐다(줌은 휠·뷰 막대·명령). - 그립 편집: 선택 객체에 그립을 그리고 집어서 옮긴다. 선 끝점·중점, 폴리선 정점, 사각형 모서리, 원 중심·반지름, 문자·점 기준점. 폴리선은 세그먼트 중점을 끌면 정점이 늘고 정점 위 Ctrl+클릭이면 준다. 형상 필드가 private이라 공개 생성자로 다시 만들어 바꿔 끼우고 id·도면층·색·그룹을 물려받는다. - 자동 백업·복구: 5초 디바운스로 복구 전용 키에 저장하고, 시작할 때 백업이 있으면 눌러서 되살리는 안내를 띄운다. 저장(QSAVE)에 성공하면 백업을 지운다. - 선택 순환: 같은 자리를 다시 클릭하면 겹친 후보를 차례로 돌린다. - 상태막대: `극좌표 추적` 버튼이 `직교`와 같은 onClick이라 같은 일을 하고 있었다. 각각 45°·90°를 켜고 끄도록 고치고, 스냅 추적 토글과 F3·F7·F8·F10을 붙였다. - 문자 굵게·기울임을 캔버스·SVG·JSON·스타일 패널에 연결했다. - 조사표: 이미 되어 있던 5건의 표기를 정정하고, 출력·내보내기(9절)는 결재창 이후 PDF·DXF·DWG로 반영할 것이라 보류(P)로 구분해 사유를 남겼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,35 +15,40 @@ import {
|
||||
WHEEL_ZOOM_EXPONENT,
|
||||
} from '../App.consts.ts';
|
||||
import { MouseButton } from '../App.types.ts';
|
||||
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts';
|
||||
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
|
||||
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
|
||||
import { getClosestSnapPointWithinRadius } from '../helpers/get-closest-snap-point.ts';
|
||||
import {
|
||||
getActiveToolActor,
|
||||
getCanvas,
|
||||
getEntities,
|
||||
getLastStateInstructions,
|
||||
getPanStartLocation,
|
||||
getSnapEnabled,
|
||||
getScreenCanvasDrawController,
|
||||
getSelectedEntities,
|
||||
getSnapPoint,
|
||||
getSnapPointOnAngleGuide,
|
||||
redo,
|
||||
setGhostHelperEntities,
|
||||
setHighlightedEntityIds,
|
||||
setPanStartLocation,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawCursor,
|
||||
undo,
|
||||
} from '../state.ts';
|
||||
import {
|
||||
describeCommand,
|
||||
matchCommandPrefixes,
|
||||
resolveCommandInput,
|
||||
} from '../commands/registry.ts';
|
||||
import { runCommand } from '../commands/run-command.ts';
|
||||
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts';
|
||||
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
|
||||
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
|
||||
import { getClosestSnapPointWithinRadius } from '../helpers/get-closest-snap-point.ts';
|
||||
import {
|
||||
getActiveToolActor,
|
||||
getAngleStep,
|
||||
getCanvas,
|
||||
getEntities,
|
||||
getGridEnabled,
|
||||
getLastStateInstructions,
|
||||
getPanStartLocation,
|
||||
getScreenCanvasDrawController,
|
||||
getSelectedEntities,
|
||||
getSnapEnabled,
|
||||
getSnapPoint,
|
||||
getSnapPointOnAngleGuide,
|
||||
redo,
|
||||
setAngleStep,
|
||||
setGhostHelperEntities,
|
||||
setGridEnabled,
|
||||
setHighlightedEntityIds,
|
||||
setPanStartLocation,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawCursor,
|
||||
setSnapEnabled,
|
||||
undo,
|
||||
} from '../state.ts';
|
||||
import { Tool } from '../tools.ts';
|
||||
import {
|
||||
type AbsolutePointInputEvent,
|
||||
@@ -55,8 +60,19 @@ import {
|
||||
} from '../tools/tool.types.ts';
|
||||
|
||||
const NUMBER_REGEXP = /^[0-9]+([.][0-9]+)?$/;
|
||||
const ABSOLUTE_POINT_REGEXP = /^([0-9]+([.][0-9]+)?)\s*,\s*([0-9]+([.][0-9]+)?)$/;
|
||||
const RELATIVE_POINT_REGEXP = /^@([0-9]+([.][0-9]+)?)\s*,\s*([0-9]+([.][0-9]+)?)$/;
|
||||
/** 부호 있는 실수 한 개 (좌표는 음수가 될 수 있다) */
|
||||
const SIGNED = '(-?[0-9]+(?:[.][0-9]+)?)';
|
||||
const ABSOLUTE_POINT_REGEXP = new RegExp(`^${SIGNED}[ ]*,[ ]*${SIGNED}$`);
|
||||
const RELATIVE_POINT_REGEXP = new RegExp(`^@${SIGNED}[ ]*,[ ]*${SIGNED}$`);
|
||||
/** 극좌표 — 거리<각도(도). `@`가 붙으면 직전 점 기준 */
|
||||
const ABSOLUTE_POLAR_REGEXP = new RegExp(`^${SIGNED}[ ]*<[ ]*${SIGNED}$`);
|
||||
const RELATIVE_POLAR_REGEXP = new RegExp(`^@${SIGNED}[ ]*<[ ]*${SIGNED}$`);
|
||||
|
||||
/** 거리·각도(도)를 x·y 변위로 바꾼다 */
|
||||
function polarToPoint(distance: number, degrees: number): Point {
|
||||
const radians = (degrees * Math.PI) / 180;
|
||||
return new Point(distance * Math.cos(radians), distance * Math.sin(radians));
|
||||
}
|
||||
|
||||
export class InputController {
|
||||
private text = '';
|
||||
@@ -320,6 +336,29 @@ export class InputController {
|
||||
}
|
||||
if (evt.key === 'F11') {
|
||||
// F11 => toggle fullscreen
|
||||
// ponytail: AutoCAD는 F11이 객체 스냅 추적이지만 브라우저 전체화면이 우선이다.
|
||||
// 스냅 추적은 상태막대 버튼으로 켜고 끈다.
|
||||
return;
|
||||
}
|
||||
// 제도 보조 토글 (AutoCAD 상태막대 기능키)
|
||||
if (evt.key === 'F3') {
|
||||
evt.preventDefault();
|
||||
setSnapEnabled(!getSnapEnabled());
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F7') {
|
||||
evt.preventDefault();
|
||||
setGridEnabled(!getGridEnabled());
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F8') {
|
||||
evt.preventDefault();
|
||||
setAngleStep(getAngleStep() === 90 ? 0 : 90);
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F10') {
|
||||
evt.preventDefault();
|
||||
setAngleStep(getAngleStep() === 45 ? 0 : 45);
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'Tab') {
|
||||
@@ -370,19 +409,9 @@ export class InputController {
|
||||
} else if (evt.key === 'ArrowRight') {
|
||||
// Move the screen right
|
||||
getScreenCanvasDrawController().setScreenOffset(this.getScreenPanStep('right', evt.shiftKey));
|
||||
} else if (evt.key === '+') {
|
||||
// Zoom in
|
||||
// TODO keep the center of the screen centered during zoom
|
||||
getScreenCanvasDrawController().setScreenScale(
|
||||
getScreenCanvasDrawController().getScreenScale() * 1.1
|
||||
);
|
||||
} else if (evt.key === '-') {
|
||||
// Zoom in
|
||||
// TODO keep the center of the screen centered during zoom
|
||||
getScreenCanvasDrawController().setScreenScale(
|
||||
getScreenCanvasDrawController().getScreenScale() * 0.9
|
||||
);
|
||||
} else if (evt.key?.length === 1) {
|
||||
// +·-는 확대·축소 단축키로 쓰지 않는다 — 음수 좌표(-100,-50)의 첫 글자를
|
||||
// 먹어 버렸다. 줌은 휠·뷰 막대·ZOOMIN/ZOOMOUT 명령으로 한다.
|
||||
// User entered a single character => add to input field text
|
||||
this.text += evt.key.toUpperCase();
|
||||
}
|
||||
@@ -472,7 +501,7 @@ export class InputController {
|
||||
return;
|
||||
}
|
||||
const x = Number.parseFloat(match[1]);
|
||||
const y = Number.parseFloat(match[3]);
|
||||
const y = Number.parseFloat(match[2]);
|
||||
getActiveToolActor()?.send({
|
||||
type: ActorEvent.ABSOLUTE_POINT_INPUT,
|
||||
value: new Point(x, y),
|
||||
@@ -489,12 +518,34 @@ export class InputController {
|
||||
return;
|
||||
}
|
||||
const x = Number.parseFloat(match[1]);
|
||||
const y = Number.parseFloat(match[3]);
|
||||
const y = Number.parseFloat(match[2]);
|
||||
getActiveToolActor()?.send({
|
||||
type: ActorEvent.RELATIVE_POINT_INPUT,
|
||||
value: new Point(x, y),
|
||||
} as RelativePointInputEvent);
|
||||
this.text = '';
|
||||
} else if (RELATIVE_POLAR_REGEXP.test(this.text)) {
|
||||
// 직전 점에서 거리·각도로 이동. 예: @100<45
|
||||
const match = RELATIVE_POLAR_REGEXP.exec(this.text);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
getActiveToolActor()?.send({
|
||||
type: ActorEvent.RELATIVE_POINT_INPUT,
|
||||
value: polarToPoint(Number.parseFloat(match[1]), Number.parseFloat(match[2])),
|
||||
} as RelativePointInputEvent);
|
||||
this.text = '';
|
||||
} else if (ABSOLUTE_POLAR_REGEXP.test(this.text)) {
|
||||
// 원점에서 거리·각도로 지정한 점. 예: 100<45
|
||||
const match = ABSOLUTE_POLAR_REGEXP.exec(this.text);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
getActiveToolActor()?.send({
|
||||
type: ActorEvent.ABSOLUTE_POINT_INPUT,
|
||||
value: polarToPoint(Number.parseFloat(match[1]), Number.parseFloat(match[2])),
|
||||
} as AbsolutePointInputEvent);
|
||||
this.text = '';
|
||||
} else {
|
||||
console.log('TEXT_INPUT: ', {
|
||||
text: this.text,
|
||||
|
||||
Reference in New Issue
Block a user