feat(B07): 자리표 칸 크기 조절·그림 비율 유지·편집 중 실제 값 미리보기
- 텍스트 자리표에 칸 크기(boxWidth/boxHeight) 도입 — basePoint 를 칸 중심으로 두고 가로·세로 가운데 정렬, 선택 시 칸 테두리 표시, DXF 내보내기도 중앙 정렬로 반영
- 그림은 칸 안에서 비율을 유지하며 맞춤(letterbox) — 칸을 늘려도 로고·서명이 찌그러지지 않음
- 자리표 패널에 선택 항목 칸 크기(가로·세로 mm) 입력 추가
- 도각 편집 중 자리표에 실제 값 미리보기 — 서버가 표제란 값(공사명·회사·담당자·로고·서명)을 함께 내려주고 화면만 값으로 표시, 저장값은 {{키}} 토큰 유지
- title_block_fields 공개화 및 frame-template 응답에 fields 추가
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import { TextEntity } from '../entities/TextEntity';
|
||||
import {
|
||||
getActiveLayerId,
|
||||
getEntities,
|
||||
getFrameFields,
|
||||
getScreenCanvasDrawController,
|
||||
getSelectedEntities,
|
||||
isFrameEditMode,
|
||||
setEntities,
|
||||
} from '../state';
|
||||
@@ -43,6 +45,9 @@ const TEXT_TOKENS = [
|
||||
const IMAGE_TOKENS = ['회사로고', '과업책임자서명', '분야별책임자서명', '설계자서명'] as const;
|
||||
|
||||
const TEXT_SIZE_MM = 5;
|
||||
// 자리표가 차지하는 칸 기본 크기(mm). 놓은 뒤 아래 「칸 크기」에서 고친다.
|
||||
const TEXT_BOX_WIDTH_MM = 60;
|
||||
const TEXT_BOX_HEIGHT_MM = 10;
|
||||
const IMAGE_WIDTH_MM = 32;
|
||||
const IMAGE_HEIGHT_MM = 16;
|
||||
|
||||
@@ -58,7 +63,11 @@ function addTextPlaceholder(token: string): void {
|
||||
const entity = new TextEntity(getActiveLayerId(), `{{${token}}}`, center, {
|
||||
fontSize: TEXT_SIZE_MM,
|
||||
textAlign: 'center',
|
||||
boxWidth: TEXT_BOX_WIDTH_MM,
|
||||
boxHeight: TEXT_BOX_HEIGHT_MM,
|
||||
});
|
||||
// 편집 중에는 실제 값을 보여 준다 — 저장값은 토큰 그대로다.
|
||||
entity.previewLabel = getFrameFields()[token] ?? null;
|
||||
setEntities([...getEntities(), entity], true);
|
||||
}
|
||||
|
||||
@@ -82,18 +91,49 @@ async function addImagePlaceholder(token: string): Promise<void> {
|
||||
layerId: getActiveLayerId(),
|
||||
shapeData: { points, imageData: `{{${token}}}` },
|
||||
} as Parameters<typeof ImageEntity.fromJson>[0]);
|
||||
const preview = getFrameFields()[token];
|
||||
if (preview) entity.setPreviewImage(preview);
|
||||
setEntities([...getEntities(), entity], true);
|
||||
}
|
||||
|
||||
/** 지금 고른 자리표 하나 — 칸 크기를 고칠 대상. 없으면 null. */
|
||||
function selectedPlaceholder(): TextEntity | ImageEntity | null {
|
||||
const selected = getSelectedEntities();
|
||||
if (selected.length !== 1) return null;
|
||||
const entity = selected[0];
|
||||
if (entity instanceof TextEntity && entity.getLabel().includes('{{')) return entity;
|
||||
if (entity instanceof ImageEntity && entity.isPlaceholder()) return entity;
|
||||
return null;
|
||||
}
|
||||
|
||||
function boxSizeOf(entity: TextEntity | ImageEntity): { width: number; height: number } {
|
||||
const box = entity.getBoundingBox();
|
||||
return { width: Math.round(box.width * 10) / 10, height: Math.round(box.height * 10) / 10 };
|
||||
}
|
||||
|
||||
export const FramePlaceholderPanel: FC = () => {
|
||||
const [visible, setVisible] = useState(isFrameEditMode());
|
||||
const [picked, setPicked] = useState<TextEntity | ImageEntity | null>(null);
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const refresh = useCallback(() => setVisible(isFrameEditMode()), []);
|
||||
const refresh = useCallback(() => {
|
||||
setVisible(isFrameEditMode());
|
||||
const entity = selectedPlaceholder();
|
||||
setPicked(entity);
|
||||
if (entity) setSize(boxSizeOf(entity));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
window.addEventListener(HtmlEvent.UPDATE_STATE, refresh);
|
||||
return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh);
|
||||
}, [refresh]);
|
||||
|
||||
const applySize = (width: number, height: number): void => {
|
||||
if (!picked) return;
|
||||
setSize({ width, height });
|
||||
picked.setBoxSize(width, height);
|
||||
setEntities([...getEntities()], true);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
@@ -115,7 +155,7 @@ export const FramePlaceholderPanel: FC = () => {
|
||||
))}
|
||||
</div>
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="cad-frame-tokens__title">그림</span>
|
||||
<span className="cad-frame-tokens__title">그림 (칸에 비율 그대로 들어감)</span>
|
||||
{IMAGE_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
@@ -127,6 +167,29 @@ export const FramePlaceholderPanel: FC = () => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{picked && (
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="cad-frame-tokens__title">고른 자리표 칸 크기 (mm)</span>
|
||||
<label className="cad-frame-tokens__size">
|
||||
가로
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={size.width}
|
||||
onChange={(event) => applySize(Number(event.target.value), size.height)}
|
||||
/>
|
||||
</label>
|
||||
<label className="cad-frame-tokens__size">
|
||||
세로
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={size.height}
|
||||
onChange={(event) => applySize(size.width, Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user