/** * 블록 라이브러리 — 사용자가 그린 객체 묶음을 이름으로 저장해 두고 다른 도면에서도 삽입한다. * 도면 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, notifyWindow } 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 = () => notifyWindow(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 { 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 { 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'); }