feat(B07): 그린 것을 블록으로 저장해 다시 놓고, 사진 넣기를 되살린다
조사표 4·7절 검토 결과를 반영한다. - 블록 라이브러리: 선택한 객체를 이름 붙여 저장(BLOCK)하고 팔레트에서 골라 클릭 한 번으로 놓는다(INSERT). 정의-참조 링크 없는 클론 방식이라 새 엔티티 타입 없이 기존 직렬화·groupId를 그대로 쓴다. 기준점은 선택 영역 중심. 라이브러리는 도면과 분리해 브라우저에 두고, 유실 대비로 파일 내보내기· 가져오기를 붙였다. - 사진 넣기: IMAGEATTACH가 파일 선택창을 띄우지 않아 도구가 FILE_SELECTED를 영원히 기다리고 있었다. 도구형 명령도 run()을 함께 부르게 하고 파일 선택을 붙여 되살렸다. - 조사표 4·7절의 반영 등급·반영 열을 실제 코드에 맞춰 정정하고, 반영하지 않기로 한 항목의 사유를 절마다 남겼다. 7절은 신규 구현 없음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -853,3 +853,66 @@ body > canvas[data-id="canvas"] {
|
||||
color: var(--cad-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 블록 라이브러리 팔레트 (INSERT/BLOCK) */
|
||||
.cad-block-library {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
top: calc(var(--cad-title-height) + var(--cad-ribbon-height) + 12px);
|
||||
right: 14px;
|
||||
width: 240px;
|
||||
padding: 8px 10px 10px;
|
||||
background: var(--cad-chrome-raised);
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.cad-block-library__empty {
|
||||
margin: 0;
|
||||
color: var(--cad-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.cad-block-library__list {
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
}
|
||||
.cad-block-library__list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.cad-block-library__list li button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--cad-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
.cad-block-library__name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 3px 4px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cad-block-library__name:hover {
|
||||
background: var(--cad-chrome-sunken);
|
||||
}
|
||||
.cad-block-library__actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.cad-block-library__actions button {
|
||||
flex: 1;
|
||||
padding: 4px 0;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 블록 라이브러리 — 사용자가 그린 객체 묶음을 이름으로 저장해 두고 다른 도면에서도 삽입한다.
|
||||
* 도면 JSON과 분리해 브라우저에 두므로 도면을 바꿔도 남는다.
|
||||
* ponytail: 정의-참조 링크 없는 클론 방식. 정의를 고쳐도 이미 삽입한 객체는 그대로다.
|
||||
* 일괄 갱신이 필요해지면 BlockEntity(참조 엔티티)로 올린다.
|
||||
*/
|
||||
import { compact } from 'es-toolkit';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { toast } from 'react-toastify';
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import type { Entity, JsonEntity } from '../entities/Entity';
|
||||
import { getBoundingBoxOfMultipleEntities } from '../helpers/get-bounding-box-of-multiple-entities';
|
||||
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json';
|
||||
import { getActiveLayerId } from '../state';
|
||||
|
||||
const STORAGE_KEY = 'aislo-cad-block-library';
|
||||
|
||||
export interface BlockDefinition {
|
||||
name: string;
|
||||
/** 삽입 기준점 (선택 영역 바운딩박스 중심) */
|
||||
basePoint: { x: number; y: number };
|
||||
entities: JsonEntity[];
|
||||
}
|
||||
|
||||
let blocks: BlockDefinition[] | null = null;
|
||||
|
||||
const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
|
||||
function load(): BlockDefinition[] {
|
||||
if (blocks) return blocks;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
blocks = raw ? (JSON.parse(raw) as BlockDefinition[]) : [];
|
||||
} catch {
|
||||
blocks = [];
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function persist(): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(blocks ?? []));
|
||||
} catch {
|
||||
toast.error('블록 라이브러리를 저장하지 못했습니다. 파일로 내보내 보관하십시오.');
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
export const getBlocks = (): BlockDefinition[] => load();
|
||||
|
||||
export const getBlockByName = (name: string): BlockDefinition | undefined =>
|
||||
load().find((block) => block.name === name);
|
||||
|
||||
/** 선택 객체를 이름 붙여 라이브러리에 넣는다. 같은 이름이면 덮어쓴다. */
|
||||
export async function saveBlockFromEntities(name: string, entities: Entity[]): Promise<void> {
|
||||
const jsonEntities = compact(await Promise.all(entities.map((entity) => entity.toJson())));
|
||||
if (!jsonEntities.length) {
|
||||
toast.error('저장할 객체가 없습니다.');
|
||||
return;
|
||||
}
|
||||
const box = getBoundingBoxOfMultipleEntities(entities);
|
||||
const definition: BlockDefinition = {
|
||||
name,
|
||||
basePoint: { x: (box.minX + box.maxX) / 2, y: (box.minY + box.maxY) / 2 },
|
||||
entities: jsonEntities,
|
||||
};
|
||||
const library = load();
|
||||
const index = library.findIndex((block) => block.name === name);
|
||||
if (index >= 0) library[index] = definition;
|
||||
else library.push(definition);
|
||||
persist();
|
||||
}
|
||||
|
||||
export function deleteBlock(name: string): void {
|
||||
blocks = load().filter((block) => block.name !== name);
|
||||
persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 정의를 실제 객체로 푼다. 정의 좌표 그대로 돌려주므로 삽입 도구가 커서까지 옮긴다.
|
||||
* 새 id·현재 도면층·공통 groupId를 줘서 한 덩어리로 다뤄진다.
|
||||
*/
|
||||
export async function deserializeBlock(block: BlockDefinition): Promise<Entity[]> {
|
||||
const { entities } = await getEntitiesAndLayersFromJsonObject({
|
||||
entities: block.entities,
|
||||
layers: [],
|
||||
});
|
||||
const groupId = crypto.randomUUID();
|
||||
const layerId = getActiveLayerId();
|
||||
for (const entity of entities) {
|
||||
entity.id = crypto.randomUUID();
|
||||
entity.layerId = layerId;
|
||||
entity.groupId = groupId;
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
export function exportBlocksToFile(): void {
|
||||
const blob = new Blob([JSON.stringify(load(), null, 2)], { type: 'text/json;charset=utf-8' });
|
||||
saveAs(blob, 'aislo-cad-blocks.json');
|
||||
}
|
||||
|
||||
export function importBlocksFromFile(file: File): void {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
try {
|
||||
const imported = JSON.parse(reader.result as string) as BlockDefinition[];
|
||||
if (!Array.isArray(imported)) throw new Error('블록 목록이 아닙니다.');
|
||||
const library = load();
|
||||
for (const block of imported) {
|
||||
if (!block?.name || !Array.isArray(block.entities)) continue;
|
||||
const index = library.findIndex((existing) => existing.name === block.name);
|
||||
if (index >= 0) library[index] = block;
|
||||
else library.push(block);
|
||||
}
|
||||
persist();
|
||||
toast.success(`블록 ${imported.length}개를 가져왔습니다.`);
|
||||
} catch {
|
||||
toast.error('블록 파일을 읽지 못했습니다.');
|
||||
}
|
||||
});
|
||||
reader.readAsText(file, 'utf-8');
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
/** 삽입 탭 명령 (조사표 4절 중 구현분) */
|
||||
import type { CadCommand } from './command.types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { saveBlockFromEntities } from '../blocks/block-library';
|
||||
import { setBlockLibraryVisible } from '../components/ui-state';
|
||||
import { importImageFromFile } from '../helpers/import-export-handlers/import-image-from-file';
|
||||
import { pickFile } from '../helpers/pick-file';
|
||||
import { getActiveToolActor, getSelectedEntities } from '../state';
|
||||
import { Tool } from '../tools';
|
||||
import { imageImportToolStateMachine } from '../tools/image-import-tool';
|
||||
import { ActorEvent, type FileSelectedEvent } from '../tools/tool.types';
|
||||
import type { CadCommand } from './command.types';
|
||||
|
||||
export const INSERT_COMMANDS: CadCommand[] = [
|
||||
{
|
||||
@@ -12,5 +19,43 @@ export const INSERT_COMMANDS: CadCommand[] = [
|
||||
hint: '래스터 이미지를 도면에 배치한다',
|
||||
tool: Tool.IMAGE_IMPORT,
|
||||
machine: imageImportToolStateMachine,
|
||||
// 도구가 FILE_SELECTED를 기다리므로 파일 선택창을 띄워 이미지를 넣어 준다
|
||||
run: () => {
|
||||
pickFile('image/*', async (file) => {
|
||||
const image = await importImageFromFile(file);
|
||||
const event: FileSelectedEvent = { type: ActorEvent.FILE_SELECTED, image };
|
||||
getActiveToolActor()?.send(event);
|
||||
});
|
||||
return '이미지 파일 선택';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'BLOCK',
|
||||
label: '블록 작성',
|
||||
aliases: ['B'],
|
||||
glyph: '🧩',
|
||||
hint: '선택한 객체를 이름 붙여 블록 라이브러리에 저장한다',
|
||||
needsSelection: true,
|
||||
run: () => {
|
||||
const entities = getSelectedEntities();
|
||||
const name = window.prompt('블록 이름', '')?.trim();
|
||||
if (!name) return '블록 작성 취소';
|
||||
void saveBlockFromEntities(name, entities).then(() => {
|
||||
toast.success(`블록 '${name}' 저장 (객체 ${entities.length}개)`);
|
||||
setBlockLibraryVisible(true);
|
||||
});
|
||||
return `블록 '${name}' 저장`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INSERT',
|
||||
label: '블록 삽입',
|
||||
aliases: ['I'],
|
||||
glyph: '📥',
|
||||
hint: '라이브러리에 저장한 블록을 도면에 놓는다',
|
||||
run: () => {
|
||||
setBlockLibraryVisible(true);
|
||||
return '블록 라이브러리';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** 명령 실행기 — 리본 버튼·명령행·단축키가 모두 이 경로로 들어온다. */
|
||||
import { toast } from 'react-toastify';
|
||||
import { Actor } from 'xstate';
|
||||
import type { CadCommand } from './command.types';
|
||||
import { getCommandById, resolveCommandInput } from './registry';
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import { getSelectedEntities, setActiveToolActor } from '../state';
|
||||
import type { CadCommand } from './command.types';
|
||||
import { getCommandById, resolveCommandInput } from './registry';
|
||||
|
||||
const COMMAND_HISTORY_LIMIT = 200;
|
||||
|
||||
@@ -34,6 +34,8 @@ export function runCommand(command: CadCommand): string {
|
||||
|
||||
if (command.machine) {
|
||||
setActiveToolActor(new Actor(command.machine));
|
||||
// 도구를 켠 뒤 준비 동작이 필요한 명령(파일 선택 등)은 run()도 함께 부른다
|
||||
command.run?.();
|
||||
log(`${command.id} ${command.label}`);
|
||||
return command.id;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/** 블록 라이브러리 팔레트 — 저장한 블록을 고르면 삽입 도구가 켜진다 (INSERT/BLOCK) */
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
deleteBlock,
|
||||
exportBlocksToFile,
|
||||
getBlocks,
|
||||
importBlocksFromFile,
|
||||
} from '../blocks/block-library';
|
||||
import { pickFile } from '../helpers/pick-file';
|
||||
import { startBlockInsert } from '../tools/insert-block-tool';
|
||||
import { isBlockLibraryVisible, setBlockLibraryVisible } from './ui-state';
|
||||
|
||||
export const BlockLibraryPanel: FC = () => {
|
||||
if (!isBlockLibraryVisible()) return null;
|
||||
|
||||
const blocks = getBlocks();
|
||||
|
||||
return (
|
||||
<div className="cad-block-library controls">
|
||||
<div className="cad-quick-properties__header">
|
||||
<span>블록 라이브러리</span>
|
||||
<button type="button" onClick={() => setBlockLibraryVisible(false)} title="닫기">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{blocks.length === 0 ? (
|
||||
<p className="cad-block-library__empty">객체를 선택하고 BLOCK 명령으로 저장하십시오.</p>
|
||||
) : (
|
||||
<ul className="cad-block-library__list">
|
||||
{blocks.map((block) => (
|
||||
<li key={block.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="cad-block-library__name"
|
||||
title={`객체 ${block.entities.length}개 — 클릭하면 삽입`}
|
||||
onClick={() => {
|
||||
setBlockLibraryVisible(false);
|
||||
void startBlockInsert(block);
|
||||
}}
|
||||
>
|
||||
{block.name}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="삭제"
|
||||
onClick={() => {
|
||||
if (window.confirm(`블록 '${block.name}'을 삭제하시겠습니까?`)) {
|
||||
deleteBlock(block.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="cad-block-library__actions">
|
||||
<button type="button" onClick={() => exportBlocksToFile()}>
|
||||
파일로 내보내기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pickFile('application/json', (file) => importBlocksFromFile(file))}
|
||||
>
|
||||
파일에서 가져오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { type FC, useEffect, useState } from 'react';
|
||||
import { CommandLine } from './CommandLine';
|
||||
import { InspectorPanel } from './InspectorPanel';
|
||||
import { QuickProperties } from './QuickProperties';
|
||||
import { BlockLibraryPanel } from './BlockLibraryPanel';
|
||||
import { LayersWidget, PropertiesWidget, TextStyleWidget } from './RibbonWidgets';
|
||||
import { QuickAccessBar } from './QuickAccessBar';
|
||||
import { Ribbon } from '../ribbon/Ribbon';
|
||||
@@ -51,6 +52,7 @@ export const Toolbar: FC = () => {
|
||||
/>
|
||||
<ViewControls />
|
||||
<QuickProperties />
|
||||
<BlockLibraryPanel />
|
||||
{commandLineVisible && <CommandLine />}
|
||||
<StatusBar
|
||||
commandLineVisible={commandLineVisible}
|
||||
|
||||
@@ -10,6 +10,7 @@ let inspectorTab: InspectorTab = 'layers';
|
||||
let inspectorCollapsed = false;
|
||||
let quickPropertiesVisible = false;
|
||||
let activeRibbonTab = 'home';
|
||||
let blockLibraryVisible = false;
|
||||
|
||||
const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
|
||||
@@ -17,6 +18,7 @@ export const getInspectorTab = () => inspectorTab;
|
||||
export const isInspectorCollapsed = () => inspectorCollapsed;
|
||||
export const isQuickPropertiesVisible = () => quickPropertiesVisible;
|
||||
export const getActiveRibbonTab = () => activeRibbonTab;
|
||||
export const isBlockLibraryVisible = () => blockLibraryVisible;
|
||||
|
||||
export function setActiveRibbonTab(tabId: string): void {
|
||||
activeRibbonTab = tabId;
|
||||
@@ -38,3 +40,8 @@ export function setQuickPropertiesVisible(visible: boolean): void {
|
||||
quickPropertiesVisible = visible;
|
||||
notify();
|
||||
}
|
||||
|
||||
export function setBlockLibraryVisible(visible: boolean): void {
|
||||
blockLibraryVisible = visible;
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 파일 선택창을 띄우고 고른 파일 하나를 돌려준다. 취소하면 아무 일도 없다. */
|
||||
export function pickFile(accept: string, onPicked: (file: File) => void): void {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = accept;
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) onPicked(file);
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
@@ -143,7 +143,10 @@ export const RIBBON_TABS: RibbonTab[] = [
|
||||
{
|
||||
id: 'insert',
|
||||
label: '삽입',
|
||||
panels: [{ label: '참조', commands: ['IMAGEATTACH'] }],
|
||||
panels: [
|
||||
{ label: '블록', big: ['INSERT'], commands: ['BLOCK'] },
|
||||
{ label: '참조', commands: ['IMAGEATTACH'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'annotate',
|
||||
@@ -182,9 +185,7 @@ export const RIBBON_TABS: RibbonTab[] = [
|
||||
{
|
||||
id: 'view',
|
||||
label: '뷰',
|
||||
panels: [
|
||||
{ label: '탐색', big: ['ZOOM'], commands: ['ZOOMIN', 'ZOOMOUT', 'PAN', 'REGEN'] },
|
||||
],
|
||||
panels: [{ label: '탐색', big: ['ZOOM'], commands: ['ZOOMIN', 'ZOOMOUT', 'PAN', 'REGEN'] }],
|
||||
},
|
||||
{
|
||||
id: 'output',
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum Tool {
|
||||
SCALE = 'SCALE',
|
||||
ERASER = 'ERASER',
|
||||
IMAGE_IMPORT = 'IMAGE_IMPORT',
|
||||
INSERT_BLOCK = 'INSERT_BLOCK',
|
||||
ROTATE = 'ROTATE',
|
||||
ALIGN_LEFT = 'ALIGN_LEFT',
|
||||
ALIGN_RIGHT = 'ALIGN_RIGHT',
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 블록 삽입 도구 — 라이브러리에서 고른 블록을 커서에 달고 다니다 클릭 한 번으로 놓는다.
|
||||
* ponytail: 1:1·0도로만 놓는다. 회전·축척은 기존 ROTATE·SCALE 명령으로 한다.
|
||||
*/
|
||||
import type { Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Actor, assign, createMachine } from 'xstate';
|
||||
import { type BlockDefinition, deserializeBlock } from '../blocks/block-library';
|
||||
import type { Entity } from '../entities/Entity';
|
||||
import { getPointFromEvent } from '../helpers/get-point-from-event';
|
||||
import {
|
||||
addEntities,
|
||||
setActiveToolActor,
|
||||
setAngleGuideOriginPoint,
|
||||
setGhostHelperEntities,
|
||||
setSelectedEntityIds,
|
||||
setShouldDrawHelpers,
|
||||
} from '../state';
|
||||
import { Tool } from '../tools';
|
||||
import { selectToolStateMachine } from './select-tool';
|
||||
import { ActorEvent, type PointInputEvent, type StateEvent, type ToolContext } from './tool.types';
|
||||
|
||||
/** 삽입 대기 중인 객체와 그 객체들이 지금 놓여 있는 기준점 */
|
||||
let pendingEntities: Entity[] = [];
|
||||
let anchor: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
function movePendingTo(x: number, y: number): void {
|
||||
for (const entity of pendingEntities) entity.move(x - anchor.x, y - anchor.y);
|
||||
anchor = { x, y };
|
||||
}
|
||||
|
||||
/** 라이브러리 팔레트에서 블록을 고르면 부른다. 객체를 풀어 두고 삽입 도구를 켠다. */
|
||||
export async function startBlockInsert(block: BlockDefinition): Promise<void> {
|
||||
try {
|
||||
pendingEntities = await deserializeBlock(block);
|
||||
} catch {
|
||||
toast.error(`블록 '${block.name}'을 풀지 못했습니다.`);
|
||||
return;
|
||||
}
|
||||
if (!pendingEntities.length) {
|
||||
toast.error(`블록 '${block.name}'에 객체가 없습니다.`);
|
||||
return;
|
||||
}
|
||||
anchor = { ...block.basePoint };
|
||||
setActiveToolActor(new Actor(insertBlockToolStateMachine));
|
||||
}
|
||||
|
||||
export enum InsertBlockState {
|
||||
INIT = 'INIT',
|
||||
WAITING_FOR_INSERTION_POINT = 'WAITING_FOR_INSERTION_POINT',
|
||||
}
|
||||
|
||||
export enum InsertBlockAction {
|
||||
INIT_INSERT_BLOCK_TOOL = 'INIT_INSERT_BLOCK_TOOL',
|
||||
DRAW_TEMP_BLOCK = 'DRAW_TEMP_BLOCK',
|
||||
PLACE_BLOCK = 'PLACE_BLOCK',
|
||||
SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL',
|
||||
}
|
||||
|
||||
export const insertBlockToolStateMachine = createMachine(
|
||||
{
|
||||
types: {} as { context: ToolContext; events: StateEvent },
|
||||
context: { type: Tool.INSERT_BLOCK },
|
||||
initial: InsertBlockState.INIT,
|
||||
states: {
|
||||
[InsertBlockState.INIT]: {
|
||||
always: {
|
||||
actions: InsertBlockAction.INIT_INSERT_BLOCK_TOOL,
|
||||
target: InsertBlockState.WAITING_FOR_INSERTION_POINT,
|
||||
},
|
||||
},
|
||||
[InsertBlockState.WAITING_FOR_INSERTION_POINT]: {
|
||||
description: '블록을 놓을 위치를 지정한다',
|
||||
meta: { instructions: '블록을 놓을 위치를 지정하십시오' },
|
||||
on: {
|
||||
[ActorEvent.DRAW]: { actions: InsertBlockAction.DRAW_TEMP_BLOCK },
|
||||
[ActorEvent.MOUSE_CLICK]: {
|
||||
actions: [InsertBlockAction.PLACE_BLOCK, InsertBlockAction.SWITCH_TO_SELECT_TOOL],
|
||||
},
|
||||
[ActorEvent.ABSOLUTE_POINT_INPUT]: {
|
||||
actions: [InsertBlockAction.PLACE_BLOCK, InsertBlockAction.SWITCH_TO_SELECT_TOOL],
|
||||
},
|
||||
[ActorEvent.ESC]: { actions: InsertBlockAction.SWITCH_TO_SELECT_TOOL },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
[InsertBlockAction.INIT_INSERT_BLOCK_TOOL]: assign(() => {
|
||||
setShouldDrawHelpers(true);
|
||||
setGhostHelperEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
setAngleGuideOriginPoint(null);
|
||||
return {};
|
||||
}),
|
||||
[InsertBlockAction.DRAW_TEMP_BLOCK]: ({ event }) => {
|
||||
if (!pendingEntities.length) return;
|
||||
const point = getPointFromEvent(null, event as PointInputEvent);
|
||||
movePendingTo(point.x, point.y);
|
||||
setGhostHelperEntities([...pendingEntities]);
|
||||
},
|
||||
[InsertBlockAction.PLACE_BLOCK]: ({ event }) => {
|
||||
if (!pendingEntities.length) return;
|
||||
const point: Point = getPointFromEvent(null, event as PointInputEvent);
|
||||
movePendingTo(point.x, point.y);
|
||||
addEntities(pendingEntities, true);
|
||||
pendingEntities = [];
|
||||
setGhostHelperEntities([]);
|
||||
},
|
||||
[InsertBlockAction.SWITCH_TO_SELECT_TOOL]: () => {
|
||||
pendingEntities = [];
|
||||
setGhostHelperEntities([]);
|
||||
setActiveToolActor(new Actor(selectToolStateMachine));
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@flatten-js/core': resolve('node_modules/@flatten-js/core'),
|
||||
'es-toolkit/compat': resolve('node_modules/es-toolkit/dist/compat/index.mjs'),
|
||||
'es-toolkit': resolve('node_modules/es-toolkit'),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -117,33 +117,41 @@
|
||||
|
||||
| 그룹(패널) | 기능/명령 | 단축키 | 짧은 설명 | 반영 등급 | 반영 |
|
||||
| ------------ | --------------------------------- | ----------- | ------------------------------------------------------------------------------ | --------- | ------- |
|
||||
| 블록 | 블록 삽입`INSERT` | I | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | B | ☐ |
|
||||
| 블록 | 스마트 블록 배치 | — | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | C | ☐ |
|
||||
| 블록 | 블록 교체`BREPLACE` | — | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | C | ☐ |
|
||||
| 블록 정의 | 블록 작성`BLOCK` | B | 선택 객체와 기준점으로 블록 정의를 만든다. | B | ☐ |
|
||||
| 블록 정의 | 블록 편집기`BEDIT` | BE | 블록 정의와 동적 블록 동작을 편집한다. | C | ☐ |
|
||||
| 블록 정의 | 블록 쓰기`WBLOCK` | W | 선택 객체 또는 블록을 별도 DWG로 저장한다. | C | ☐ |
|
||||
| 블록 정의 | 속성 정의`ATTDEF` | ATT | 블록에 포함할 문자 데이터 필드를 정의한다. | C | ☐ |
|
||||
| 블록 정의 | 속성 관리`BATTMAN` | — | 블록 속성의 순서·표시·기본값을 관리한다. | C | ☐ |
|
||||
| 블록 정의 | 속성 동기화`ATTSYNC` | — | 변경된 속성 정의를 기존 블록 참조에 반영한다. | C | ☐ |
|
||||
| 블록 정의 | 향상된 속성 편집`EATTEDIT` | — | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | C | ☐ |
|
||||
| 블록 | 블록 삽입`INSERT` | I | 현재 도면·최근 항목·라이브러리의 블록을 배치한다. | A | ☑ |
|
||||
| 블록 | 스마트 블록 배치 | — | 블록 팔레트로 삽입할 때 기존 동일 블록을 바탕으로 위치·회전·축척을 제안한다. | C | X |
|
||||
| 블록 | 블록 교체`BREPLACE` | — | 선택 블록을 제안 목록 또는 지정한 다른 블록으로 교체한다. | C | X |
|
||||
| 블록 정의 | 블록 작성`BLOCK` | B | 선택 객체와 기준점으로 블록 정의를 만든다. | A | ☑ |
|
||||
| 블록 정의 | 블록 편집기`BEDIT` | BE | 블록 정의와 동적 블록 동작을 편집한다. | C | X |
|
||||
| 블록 정의 | 블록 쓰기`WBLOCK` | W | 선택 객체 또는 블록을 별도 DWG로 저장한다. | C | X |
|
||||
| 블록 정의 | 속성 정의`ATTDEF` | ATT | 블록에 포함할 문자 데이터 필드를 정의한다. | C | X |
|
||||
| 블록 정의 | 속성 관리`BATTMAN` | — | 블록 속성의 순서·표시·기본값을 관리한다. | C | X |
|
||||
| 블록 정의 | 속성 동기화`ATTSYNC` | — | 변경된 속성 정의를 기존 블록 참조에 반영한다. | C | X |
|
||||
| 블록 정의 | 향상된 속성 편집`EATTEDIT` | — | 삽입된 블록 참조의 속성 값과 문자·표시 특성을 편집한다. | C | X |
|
||||
| 참조 | DWG 부착`XATTACH` | XA | 다른 DWG를 외부 참조로 연결한다. | C | X |
|
||||
| 참조 | 이미지 부착`IMAGEATTACH` | IAT | 래스터 이미지를 참조로 연결한다. | A | ☑ 기존 |
|
||||
| 참조 | 이미지 부착`IMAGEATTACH` | IAT | 래스터 이미지를 참조로 연결한다. | A | ☑ |
|
||||
| 참조 | PDF 부착`PDFATTACH` | — | PDF 페이지를 언더레이로 연결한다. | C | X |
|
||||
| 참조 | DWF/DGN 부착 | — | DWF 또는 DGN을 언더레이로 연결한다. | C | X |
|
||||
| 참조 | 점 구름 부착 | — | 스캔된 점 구름 데이터를 도면에 연결한다. | C | X |
|
||||
| 참조 | 외부 참조 팔레트`XREF` | XR | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | C | ☐ |
|
||||
| 참조 | 외부 참조 팔레트`XREF` | XR | 참조의 로드·언로드·재로드·경로·결합을 관리한다. | C | X |
|
||||
| 참조 | 참조 내부 편집`REFEDIT` | — | 블록 또는 외부 참조를 현재 도면 안에서 직접 편집한다. | C | X |
|
||||
| 참조 | 외부 참조 자르기`XCLIP` | XC | DWG 외부 참조 또는 블록의 표시 경계를 지정한다. | C | X |
|
||||
| 참조 | 이미지 자르기`IMAGECLIP` | ICL | 부착 이미지의 표시 경계를 지정한다. | B | ☐ |
|
||||
| 참조 | 이미지 자르기`IMAGECLIP` | ICL | 부착 이미지의 표시 경계를 지정한다. | C | X |
|
||||
| 참조 | PDF 자르기`PDFCLIP` | — | PDF 언더레이의 표시 경계를 지정한다. | C | X |
|
||||
| 가져오기 | PDF 가져오기`PDFIMPORT` | — | PDF의 벡터 형상·문자·채움을 도면 객체로 변환한다. | C | X |
|
||||
| 가져오기 | 가져오기`IMPORT` | IMP | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | B | X |
|
||||
| 가져오기 | 가져오기`IMPORT` | IMP | 지원되는 다른 형식의 데이터를 현재 도면으로 가져온다. | C | X |
|
||||
| 링크 및 추출 | 데이터 링크`DATALINK` | DL | Excel 등 외부 표 데이터와 도면 테이블을 연결한다. | C | X |
|
||||
| 링크 및 추출 | 데이터 추출`DATAEXTRACTION` | DX | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | B | X |
|
||||
| 링크 및 추출 | 데이터 추출`DATAEXTRACTION` | DX | 객체·블록·속성 정보를 표 또는 외부 파일로 추출한다. | C | X |
|
||||
| 링크 및 추출 | 하이퍼링크`HYPERLINK` | — | 객체에 웹·파일·도면 뷰 링크를 연결한다. | C | X |
|
||||
| 링크 및 추출 | 필드`FIELD` | — | 날짜·도면·객체 특성 등 갱신 가능한 문자 값을 삽입한다. | C | X |
|
||||
| 위치 | 지리적 위치`GEOGRAPHICLOCATION` | GEO / NORTH | 좌표계와 지도상의 도면 위치를 지정한다. | C | ☐ |
|
||||
| 위치 | 지리적 위치`GEOGRAPHICLOCATION` | GEO / NORTH | 좌표계와 지도상의 도면 위치를 지정한다. | C | X |
|
||||
|
||||
**4절 결정(2026-08-30)**: 사용자가 그린 객체를 이름 붙여 저장·재삽입하는 블록(`BLOCK`/`INSERT`)과
|
||||
사진 넣기(`IMAGEATTACH`)만 반영한다. 블록은 정의-참조 링크 없는 클론 방식이라 정의를 고쳐도 이미
|
||||
삽입한 객체는 그대로다. 라이브러리는 브라우저에 저장하고 파일로 내보내기·가져오기를 둔다.
|
||||
나머지는 DWG·데스크톱 종속이거나(외부 참조·PDF·DGN·점 구름·속성·데이터 링크·필드) 대체 수단이
|
||||
있어(`IMPORT`는 수요 없음, `DATAEXTRACTION`은 수량표 패널) 반영하지 않는다.
|
||||
`IMAGEATTACH`는 표에 반영됨으로 적혀 있었으나 파일 선택 경로가 없어 실제로는 동작하지 않았고,
|
||||
이번에 배선을 복구했다.
|
||||
|
||||
## 5. 주석 탭 — 문자·치수·지시선·표·표식
|
||||
|
||||
@@ -212,24 +220,29 @@
|
||||
| 탐색 | 줌`ZOOM` | Z | 확대·축소, 범위 전체, 윈도우 등의 방법으로 뷰를 변경한다. | A | ☑ 기존 |
|
||||
| 탐색 | 초점이동`PAN` | P | 배율을 유지한 채 화면의 관찰 위치를 이동한다. | A | ☑ 기존 |
|
||||
| 탐색 | 탐색 막대 | — | 줌·초점이동 등 화면 탐색 도구를 모아 제공한다. | A | ☑ 기존 |
|
||||
| 뷰 | 명명된 뷰`VIEW` | V | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | B | ☐ |
|
||||
| 뷰 | 새 명명 뷰`NEWVIEW` | NVIEW | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | B | ☐ |
|
||||
| 뷰 | 재생성`REGEN` | RE | 현재 뷰포트의 객체 표시를 다시 계산한다. | B | ☐ |
|
||||
| 뷰 | 전체 재생성`REGENALL` | REA | 모든 뷰포트의 객체 표시를 다시 계산한다. | C | ☐ |
|
||||
| 뷰포트 | 모형 공간 뷰포트 | — | 도면 영역을 여러 독립 관찰 창으로 나눈다. | C | ☐ |
|
||||
| 뷰포트 | 배치 뷰포트`MVIEW` | MV | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | B | ☐ |
|
||||
| 뷰포트 | 뷰포트 잠금 | — | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | C | ☐ |
|
||||
| 좌표 | UCS`UCS` | — | 사용자 좌표계의 원점과 축 방향을 설정한다. | B | ☐ |
|
||||
| 좌표 | 월드 UCS | — | 좌표계를 기본 WCS로 복원한다. | B | ☐ |
|
||||
| 팔레트 | 도구 팔레트`TOOLPALETTES` | TP / Ctrl+3 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | C | ☐ |
|
||||
| 팔레트 | 개수`COUNT` | — | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | B | ☐ |
|
||||
| 팔레트 | DesignCenter`ADCENTER` | ADC / Ctrl+2 | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | C | ☐ |
|
||||
| 팔레트 | 시트 세트 관리자 | Ctrl+4 / SSM | 여러 도면 시트의 집합·번호·출력을 관리한다. | C | ☐ |
|
||||
| 뷰 | 명명된 뷰`VIEW` | V | 현재 화면·도면층·UCS 상태를 이름으로 저장하고 복원한다. | C | X |
|
||||
| 뷰 | 새 명명 뷰`NEWVIEW` | NVIEW | 현재 화면 또는 지정 영역을 새 이름의 뷰로 저장한다. | C | X |
|
||||
| 뷰 | 재생성`REGEN` | RE | 현재 뷰포트의 객체 표시를 다시 계산한다. | A | ☑ |
|
||||
| 뷰 | 전체 재생성`REGENALL` | REA | 모든 뷰포트의 객체 표시를 다시 계산한다. | C | X |
|
||||
| 뷰포트 | 모형 공간 뷰포트 | — | 도면 영역을 여러 독립 관찰 창으로 나눈다. | C | X |
|
||||
| 뷰포트 | 배치 뷰포트`MVIEW` | MV | 종이 공간에 모형을 표시하는 출력 창을 작성한다. | C | X |
|
||||
| 뷰포트 | 뷰포트 잠금 | — | 배치 뷰포트의 축척과 관찰 상태 변경을 막는다. | C | X |
|
||||
| 좌표 | UCS`UCS` | — | 사용자 좌표계의 원점과 축 방향을 설정한다. | C | X |
|
||||
| 좌표 | 월드 UCS | — | 좌표계를 기본 WCS로 복원한다. | C | X |
|
||||
| 팔레트 | 도구 팔레트`TOOLPALETTES` | TP / Ctrl+3 | 자주 쓰는 블록·해치·명령 도구 모음을 표시한다. | C | X |
|
||||
| 팔레트 | 개수`COUNT` | — | 선택 객체·블록의 인스턴스를 세고 결과를 강조·검토한다. | C | X |
|
||||
| 팔레트 | DesignCenter`ADCENTER` | ADC / Ctrl+2 | 다른 도면의 블록·도면층·스타일을 탐색해 가져온다. | C | X |
|
||||
| 팔레트 | 시트 세트 관리자 | Ctrl+4 / SSM | 여러 도면 시트의 집합·번호·출력을 관리한다. | C | X |
|
||||
| 팔레트 | 명령행 | Ctrl+9 / CLI | 명령 입력, 옵션 선택 및 작업 기록을 표시한다. | A | ☑ 기존 |
|
||||
| 팔레트 | 명령 매크로 | — | 반복 명령 시퀀스를 검토·저장·실행한다. | C | ☐ |
|
||||
| 인터페이스 | 파일 탭 | Ctrl+Tab | 열려 있는 여러 도면 사이를 전환한다. | B | ☐ |
|
||||
| 인터페이스 | 타일·계단식 배열 | — | 여러 열린 도면 창의 배치를 조정한다. | C | ☐ |
|
||||
| 인터페이스 | 전체 화면 정리`CLEANSCREENON` | Ctrl+0 | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | B | ☐ |
|
||||
| 팔레트 | 명령 매크로 | — | 반복 명령 시퀀스를 검토·저장·실행한다. | C | X |
|
||||
| 인터페이스 | 파일 탭 | Ctrl+Tab | 열려 있는 여러 도면 사이를 전환한다. | C | X |
|
||||
| 인터페이스 | 타일·계단식 배열 | — | 여러 열린 도면 창의 배치를 조정한다. | C | X |
|
||||
| 인터페이스 | 전체 화면 정리`CLEANSCREENON` | Ctrl+0 | 도구막대와 팔레트를 숨겨 도면 영역을 최대화한다. | C | X |
|
||||
|
||||
**7절 결정(2026-08-30)**: 신규 구현 없음. `ZOOM`·`PAN`·탐색 막대·명령행·`REGEN`으로 충분하다.
|
||||
`파일 탭`은 호스트 화면이 이미 도면을 넘긴다(`requestDrawingNavigation`). `배치 뷰포트`·`모형 공간
|
||||
뷰포트`는 종이공간 개념 자체가 없어 실질 C다. 명명된 뷰·UCS·`COUNT`·전체 화면 정리는 만들 수는
|
||||
있으나 쓸 자리가 확인되지 않아 보류했다 — 필요해지면 그때 등급을 올린다.
|
||||
|
||||
## 8. 관리 탭 — 표준·사용자화·자동화
|
||||
|
||||
|
||||
Reference in New Issue
Block a user