260725_7
This commit is contained in:
@@ -1,94 +1,15 @@
|
||||
import { type FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type FC, useCallback, useEffect, useState } from 'react';
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import {
|
||||
notifyDrawingChangedByTable,
|
||||
requestDrawingNavigation,
|
||||
} from '../integration/aislo-drawing-bridge';
|
||||
import { getDesignMeta, setDesignQuantityTable } from '../state';
|
||||
|
||||
/** 편집 가능한 항목 키 (cut/fill 은 지반고·계획고에서 파생되는 읽기전용). */
|
||||
const EDITABLE_KEYS = [
|
||||
'ground',
|
||||
'planned',
|
||||
'cut_soil',
|
||||
'cut_soft_rock',
|
||||
'cut_rock',
|
||||
'tree_removal',
|
||||
'fill_slope_protection',
|
||||
'cut_slope_protection',
|
||||
'ditch_soil',
|
||||
'ditch_soft_rock',
|
||||
'ditch_rock',
|
||||
'embankment',
|
||||
'grubbing',
|
||||
'surface_grading',
|
||||
] as const;
|
||||
|
||||
function round2(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function formatValue(value: number | null | undefined): string {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? String(round2(value)) : '';
|
||||
}
|
||||
|
||||
function parseValue(raw: string): number | null {
|
||||
const text = raw.trim();
|
||||
if (!text) return null;
|
||||
const parsed = Number(text);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/** 편집 값 문자열 → 저장용 수치 테이블 (파생 cut/fill 포함) */
|
||||
function toNumericTable(values: Record<string, string>): Record<string, number | null> {
|
||||
const table: Record<string, number | null> = {};
|
||||
for (const key of EDITABLE_KEYS) table[key] = parseValue(values[key] ?? '');
|
||||
const ground = table.ground;
|
||||
const planned = table.planned;
|
||||
if (typeof ground === 'number' && typeof planned === 'number') {
|
||||
table.cut = Math.max(ground - planned, 0);
|
||||
table.fill = Math.max(planned - ground, 0);
|
||||
} else {
|
||||
table.cut = null;
|
||||
table.fill = null;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
interface ValueInputProps {
|
||||
fieldKey: (typeof EDITABLE_KEYS)[number];
|
||||
colSpan: number;
|
||||
values: Record<string, string>;
|
||||
onEdit: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
const ValueInput: FC<ValueInputProps> = ({ fieldKey, colSpan, values, onEdit }) => (
|
||||
<td className="cad-qtable__value" colSpan={colSpan}>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
value={values[fieldKey] ?? ''}
|
||||
onChange={(event) => onEdit(fieldKey, event.target.value)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
|
||||
const DerivedCell: FC<{ value: string }> = ({ value }) => (
|
||||
<td className="cad-qtable__value cad-qtable__value--derived" colSpan={1}>
|
||||
<input type="text" readOnly value={value} tabIndex={-1} />
|
||||
</td>
|
||||
);
|
||||
import { requestDrawingNavigation } from '../integration/aislo-drawing-bridge';
|
||||
import { getDesignMeta } from '../state';
|
||||
|
||||
/**
|
||||
* 횡단도 수량 산출표 (CAD 화면 내부, 하단 중심 접이식 패널).
|
||||
* 헤더(제목·측점정보·확정상태·이전/다음)는 접어도 항상 보인다.
|
||||
* 도면 내비게이션 바 (CAD 화면 하단 중심).
|
||||
* 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b07-cross-table)로
|
||||
* 렌더되므로, 여기서는 제목·측점정보·확정상태·이전/다음 이동만 담당한다.
|
||||
*/
|
||||
export const QuantityPanel: FC = () => {
|
||||
const [meta, setMeta] = useState(getDesignMeta());
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const identityRef = useRef<string>('');
|
||||
|
||||
const refresh = useCallback(() => setMeta(getDesignMeta()), []);
|
||||
useEffect(() => {
|
||||
@@ -96,35 +17,10 @@ export const QuantityPanel: FC = () => {
|
||||
return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh);
|
||||
}, [refresh]);
|
||||
|
||||
// 새 도면(측점)으로 바뀌면 입력값을 원본값으로 초기화한다.
|
||||
const identity = meta ? `${meta.kind}:${meta.title}` : '';
|
||||
useEffect(() => {
|
||||
if (!meta) return;
|
||||
if (identityRef.current === identity) return;
|
||||
identityRef.current = identity;
|
||||
const next: Record<string, string> = {};
|
||||
for (const key of EDITABLE_KEYS) next[key] = formatValue(meta.quantityTable?.[key]);
|
||||
setValues(next);
|
||||
}, [identity, meta]);
|
||||
|
||||
if (!meta) return null;
|
||||
|
||||
const handleEdit = (key: string, value: string) => {
|
||||
const nextValues = { ...values, [key]: value };
|
||||
setValues(nextValues);
|
||||
setDesignQuantityTable(toNumericTable(nextValues));
|
||||
notifyDrawingChangedByTable();
|
||||
};
|
||||
|
||||
const ground = parseValue(values.ground ?? '');
|
||||
const planned = parseValue(values.planned ?? '');
|
||||
const cutText =
|
||||
ground !== null && planned !== null ? formatValue(Math.max(ground - planned, 0)) : '';
|
||||
const fillText =
|
||||
ground !== null && planned !== null ? formatValue(Math.max(planned - ground, 0)) : '';
|
||||
|
||||
return (
|
||||
<section className="cad-qtable controls" data-collapsed={meta.kind !== 'cross' || collapsed}>
|
||||
<section className="cad-qtable controls" data-collapsed="true">
|
||||
<header className="cad-qtable__header">
|
||||
<button
|
||||
type="button"
|
||||
@@ -155,117 +51,7 @@ export const QuantityPanel: FC = () => {
|
||||
>
|
||||
›
|
||||
</button>
|
||||
{meta.kind === 'cross' && (
|
||||
<button
|
||||
type="button"
|
||||
className="cad-qtable__collapse"
|
||||
title={collapsed ? '표 펼치기' : '표 접기'}
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
>
|
||||
{collapsed ? '▲' : '▼'}
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* 표는 종단도에서도 DOM에 남겨 패널 폭을 통일한다 (CSS로만 숨김) */}
|
||||
<div className="cad-qtable__body">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
지반고
|
||||
</th>
|
||||
<ValueInput fieldKey="ground" colSpan={1} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
계획고
|
||||
</th>
|
||||
<ValueInput fieldKey="planned" colSpan={1} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
절토고
|
||||
</th>
|
||||
<DerivedCell value={cutText} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
성토고
|
||||
</th>
|
||||
<DerivedCell value={fillText} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={3}>
|
||||
흙깎기
|
||||
</th>
|
||||
<th className="cad-qtable__label">토사</th>
|
||||
<ValueInput fieldKey="cut_soil" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
지장목제거
|
||||
</th>
|
||||
<ValueInput fieldKey="tree_removal" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={3}>
|
||||
옆도랑파기
|
||||
</th>
|
||||
<th className="cad-qtable__label">토사</th>
|
||||
<ValueInput fieldKey="ditch_soil" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label">연암</th>
|
||||
<ValueInput
|
||||
fieldKey="cut_soft_rock"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={2}>
|
||||
비탈보호공
|
||||
</th>
|
||||
<th className="cad-qtable__label">성토면</th>
|
||||
<ValueInput
|
||||
fieldKey="fill_slope_protection"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label">연암</th>
|
||||
<ValueInput
|
||||
fieldKey="ditch_soft_rock"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label">보통암</th>
|
||||
<ValueInput fieldKey="cut_rock" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label">절토면</th>
|
||||
<ValueInput
|
||||
fieldKey="cut_slope_protection"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label">보통암</th>
|
||||
<ValueInput fieldKey="ditch_rock" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
흙쌓기
|
||||
</th>
|
||||
<ValueInput fieldKey="embankment" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
제근
|
||||
</th>
|
||||
<ValueInput fieldKey="grubbing" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
노면고르기
|
||||
</th>
|
||||
<ValueInput
|
||||
fieldKey="surface_grading"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export const Toolbar: FC = () => {
|
||||
const [selectedCount, setSelectedCount] = useState(0);
|
||||
const [selectedType, setSelectedType] = useState('선택 없음');
|
||||
const [instruction, setInstruction] = useState('명령을 입력하거나 도구를 선택하십시오.');
|
||||
const [panelTab, setPanelTab] = useState<'properties' | 'layers'>('properties');
|
||||
const [panelTab, setPanelTab] = useState<'properties' | 'layers'>('layers');
|
||||
const [panelCollapsed, setPanelCollapsed] = useState(false);
|
||||
const [snap, setSnap] = useState(getSnapEnabled());
|
||||
const [grid, setGrid] = useState(getGridEnabled());
|
||||
|
||||
@@ -109,6 +109,15 @@ export class TextEntity implements Entity {
|
||||
Object.assign(this.options, newOptions);
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
/** 테이블 값 셀 등 기존 텍스트 내용을 직접 수정한다 (aislo 더블클릭 편집용). */
|
||||
public setLabel(newLabel: string): void {
|
||||
this.label = newLabel;
|
||||
}
|
||||
|
||||
public getShape(): Shape | null {
|
||||
return null; // TODO see why we need to get the shape out of an entity
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { type DesignMeta, HtmlEvent } from '../App.types.ts';
|
||||
import { TextEntity } from '../entities/TextEntity.ts';
|
||||
import type { JsonDrawingFileSerialized } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
||||
import { exportEntitiesAndLayersToJsonString } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
||||
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json.ts';
|
||||
import { type DesignMeta, HtmlEvent } from '../App.types.ts';
|
||||
import {
|
||||
getCanvas,
|
||||
getDesignMeta,
|
||||
getEntities,
|
||||
getLayers,
|
||||
getScreenCanvasDrawController,
|
||||
setActiveLayerId,
|
||||
setDesignMeta,
|
||||
@@ -51,6 +56,44 @@ export function notifyDrawingChangedByTable() {
|
||||
notifyParent(AISLO_DRAWING_CHANGED_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 더블클릭한 위치의 텍스트(잠금 해제 레이어)를 찾아 내용을 즉시 편집한다.
|
||||
* CAD 테이블(측점 테이블·수량 산출표) 값 셀 수정 수단 — 잠금 레이어는 제외.
|
||||
*/
|
||||
function registerTextDoubleClickEdit() {
|
||||
const canvas = getCanvas();
|
||||
if (!canvas) return;
|
||||
canvas.addEventListener('dblclick', (event: MouseEvent) => {
|
||||
const drawController = getScreenCanvasDrawController();
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
const screenPoint = new Point(event.clientX - bounds.left, bounds.bottom - event.clientY);
|
||||
const worldPoint = drawController.targetToWorld(screenPoint);
|
||||
const lockedLayerIds = new Set(
|
||||
getLayers()
|
||||
.filter((layer) => layer.isLocked)
|
||||
.map((layer) => layer.id)
|
||||
);
|
||||
let closest: TextEntity | null = null;
|
||||
let closestDistance = Number.MAX_SAFE_INTEGER;
|
||||
for (const entity of getEntities()) {
|
||||
if (!(entity instanceof TextEntity) || lockedLayerIds.has(entity.layerId)) continue;
|
||||
const distanceInfo = entity.distanceTo(worldPoint);
|
||||
if (distanceInfo && distanceInfo[0] < closestDistance) {
|
||||
closestDistance = distanceInfo[0];
|
||||
closest = entity;
|
||||
}
|
||||
}
|
||||
if (!closest) return;
|
||||
const fontSize = closest.getTextOptions().fontSize;
|
||||
const tolerance = Math.max((closest.getLabel().length + 2) * fontSize * 0.5, fontSize * 2);
|
||||
if (closestDistance > tolerance) return;
|
||||
const nextLabel = window.prompt('값 수정', closest.getLabel());
|
||||
if (nextLabel === null || nextLabel === closest.getLabel()) return;
|
||||
closest.setLabel(nextLabel);
|
||||
setEntities([...getEntities()], true); // undo 스택 + DRAWING_CHANGED 통지
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* B07 parent page와 CAD 앱 사이의 same-origin JSON 경계다.
|
||||
* DXF/DWG 파일이나 파서 객체는 이 경계를 통과하지 않는다.
|
||||
@@ -92,6 +135,7 @@ export function registerAisloDrawingBridge() {
|
||||
window.addEventListener(HtmlEvent.DRAWING_CHANGED, () => {
|
||||
notifyParent(AISLO_DRAWING_CHANGED_MESSAGE);
|
||||
});
|
||||
registerTextDoubleClickEdit();
|
||||
|
||||
notifyParent(AISLO_DRAWING_READY_MESSAGE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user