/** * Entity Property Inspector module for dwg-dxf-viewer-sample. * Formats CAD entity properties and renders detailed property UI panels. */ export interface FormattedProperties { id: string; type: string; handle: string; layer: string; general: [string, string][]; geometry: [string, string][]; } const f2 = (v: any): string => (typeof v === 'number' ? v.toFixed(2) : (v ?? '-')); const f3 = (v: any): string => (typeof v === 'number' ? v.toFixed(3) : (v ?? '-')); const deg = (r: any): string => { if (typeof r !== 'number') return '-'; let d = (r * 180) / Math.PI; while (d < 0) d += 360; while (d >= 360) d -= 360; return d.toFixed(1) + '°'; }; const pt = (p: any): string => { if (!p) return '-'; const x = typeof p.x === 'number' ? f2(p.x) : '0.00'; const y = typeof p.y === 'number' ? f2(p.y) : '0.00'; const z = typeof p.z === 'number' && Math.abs(p.z) > 1e-6 ? `, ${f2(p.z)}` : ''; return `${x}, ${y}${z}`; }; function formatColor(colorVal: any): string { if (colorVal == null) return 'BYLAYER'; if (typeof colorVal === 'number') { if (colorVal === 256) return 'BYLAYER (256)'; if (colorVal === 0) return 'BYBLOCK (0)'; return `ACI ${colorVal}`; } return String(colorVal); } export function formatEntityProperties(entity: any): FormattedProperties { if (!entity) { return { id: '', type: '', handle: '', layer: '', general: [], geometry: [] }; } const d = entity.data || entity; const type = (entity.type || entity.typeName || 'ENTITY').toUpperCase(); const handle = entity.handle?.value ?? entity.handle ?? '-'; const layer = entity.layer ?? entity.layerName ?? '0'; const color = formatColor(entity.color ?? d.color); const linetype = entity.lineType ?? entity.linetype ?? d.lineType ?? 'BYLAYER'; const general: [string, string][] = [ ['유형 (Type)', type], ['핸들 (Handle)', String(handle)], ['레이어 (Layer)', String(layer)], ['색상 (Color)', color], ['선종류 (Linetype)', String(linetype)], ]; const geometry: [string, string][] = []; switch (type) { case 'LINE': { if (d.start && d.end) { const dx = d.end.x - d.start.x; const dy = d.end.y - d.start.y; const dz = (d.end.z || 0) - (d.start.z || 0); const len = Math.hypot(dx, dy, dz); const angle = Math.atan2(dy, dx); geometry.push(['시작점 (Start)', pt(d.start)]); geometry.push(['끝점 (End)', pt(d.end)]); geometry.push(['증분 (ΔX, ΔY)', `${f2(dx)}, ${f2(dy)}`]); geometry.push(['길이 (Length)', f3(len)]); geometry.push(['각도 (Angle)', deg(angle)]); } break; } case 'CIRCLE': { if (d.center && d.radius != null) { const r = d.radius; const diameter = r * 2; const circumference = 2 * Math.PI * r; const area = Math.PI * r * r; geometry.push(['중심점 (Center)', pt(d.center)]); geometry.push(['반지름 (Radius)', f3(r)]); geometry.push(['지름 (Diameter)', f3(diameter)]); geometry.push(['둘레 (Circumference)', f3(circumference)]); geometry.push(['넓이 (Area)', f3(area)]); } break; } case 'ARC': { if (d.center && d.radius != null) { const r = d.radius; const sa = d.startAngle ?? 0; const ea = d.endAngle ?? Math.PI * 2; let sweep = ea - sa; while (sweep < 0) sweep += Math.PI * 2; const arcLen = sweep * r; geometry.push(['중심점 (Center)', pt(d.center)]); geometry.push(['반지름 (Radius)', f3(r)]); geometry.push(['시작각 (Start Angle)', deg(sa)]); geometry.push(['끝각 (End Angle)', deg(ea)]); geometry.push(['중심각 (Sweep Angle)', deg(sweep)]); geometry.push(['호 길이 (Arc Length)', f3(arcLen)]); } break; } case 'LWPOLYLINE': case 'POLYLINE': { const rawPts = d.points ?? d.vertices ?? []; const isClosed = !!(d.closed || (d.flags & 1)); let totalLen = 0; for (let i = 0; i < rawPts.length - (isClosed ? 0 : 1); i++) { const p1 = rawPts[i]; const p2 = rawPts[(i + 1) % rawPts.length]; if (p1 && p2) { totalLen += Math.hypot(p2.x - p1.x, p2.y - p1.y); } } geometry.push(['정점 개수 (Vertices)', String(rawPts.length)]); geometry.push(['닫힘 여부 (Closed)', isClosed ? '예 (Closed)' : '아니오 (Open)']); geometry.push(['총 길이 (Total Length)', f3(totalLen)]); if (isClosed && rawPts.length >= 3) { let shoelace = 0; for (let i = 0; i < rawPts.length; i++) { const p1 = rawPts[i]; const p2 = rawPts[(i + 1) % rawPts.length]; shoelace += p1.x * p2.y - p2.x * p1.y; } geometry.push(['면적 (Area)', f3(Math.abs(shoelace) / 2)]); } if (rawPts.length > 0) { const sampleCount = Math.min(rawPts.length, 4); for (let i = 0; i < sampleCount; i++) { geometry.push([`정점 P${i + 1}`, pt(rawPts[i])]); } if (rawPts.length > sampleCount) { geometry.push(['...', `외 ${rawPts.length - sampleCount}개 정점`]); } } break; } case 'TEXT': case 'MTEXT': case 'ATTRIB': { const content = d.text ?? entity.text ?? d.textValue ?? '-'; const height = d.textHeight ?? d.height ?? entity.textHeight; const rot = d.rotationAngle ?? d.rotation ?? 0; const pos = d.insertionPoint ?? d.insertionPt ?? d.alignmentPt ?? d.pos; geometry.push(['텍스트 내용 (Content)', String(content)]); if (height != null) geometry.push(['높이 (Height)', f3(height)]); geometry.push(['회전각 (Rotation)', deg(rot)]); if (pos) geometry.push(['삽입점 (Position)', pt(pos)]); break; } case 'INSERT': { const name = d.name ?? d.blockName ?? entity.name ?? '-'; const pos = d.insertionPoint ?? d.insertionPt ?? entity.insertionPt; const sx = d.xScale ?? d.scale?.x ?? 1; const sy = d.yScale ?? d.scale?.y ?? 1; const sz = d.zScale ?? d.scale?.z ?? 1; const rot = d.rotation ?? 0; geometry.push(['블록 이름 (Block)', String(name)]); if (pos) geometry.push(['삽입점 (Position)', pt(pos)]); geometry.push(['축척 (Scale X,Y,Z)', `${f2(sx)}, ${f2(sy)}, ${f2(sz)}`]); geometry.push(['회전각 (Rotation)', deg(rot)]); break; } case 'HATCH': { const isSolid = d.solidFill ?? entity.solidFill ?? false; const pattern = d.patternName ?? d.name ?? (isSolid ? 'SOLID' : 'HATCH'); const paths = d.paths ?? entity.paths ?? []; geometry.push(['패턴 (Pattern)', String(pattern)]); geometry.push(['채우기 유형 (Type)', isSolid ? '단색 채우기 (Solid)' : '패턴 (Pattern)']); geometry.push(['경계 루프 수 (Loops)', String(paths.length)]); break; } case 'DIMENSION': case 'DIMENSION_LINEAR': case 'DIMENSION_ALIGNED': case 'DIMENSION_RADIUS': case 'DIMENSION_DIAMETER': case 'DIMENSION_ANG_3PT': case 'DIMENSION_ANG_2LN': case 'DIMENSION_ORDINATE': { const meas = d.actualMeasurement ?? entity.actualMeasurement; const textVal = d.userText || (meas != null ? f3(meas) : '-'); const midPt = d.textMidPt ?? entity.textMidPt; geometry.push(['치수 유형 (Dim Type)', type]); if (meas != null) geometry.push(['측정값 (Measurement)', f3(meas)]); geometry.push(['표시 텍스트 (Text)', String(textVal)]); if (midPt) geometry.push(['텍스트 위치 (Text Mid)', pt(midPt)]); break; } case 'ELLIPSE': { if (d.center) { geometry.push(['중심점 (Center)', pt(d.center)]); if (d.majorAxis) geometry.push(['주축 벡터 (Major Axis)', pt(d.majorAxis)]); if (d.axisRatio != null || d.ratio != null) { geometry.push(['단축 비율 (Ratio)', f3(d.axisRatio ?? d.ratio)]); } } break; } default: { let count = 0; for (const [k, v] of Object.entries(d)) { if (typeof v === 'object' || v == null || typeof v === 'function') continue; geometry.push([k, String(v)]); count++; if (count >= 12) break; } } } const id = `${type}${handle !== '-' ? ' · #' + handle : ''}`; return { id, type, handle: String(handle), layer: String(layer), general, geometry }; } function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } export function renderPropertyInspector( bodyEl: HTMLElement, entity: any, options?: { onZoomToSelection?: () => void; zoomBtnEl?: HTMLElement | null } ): void { if (!bodyEl) return; if (!entity) { if (options?.zoomBtnEl) options.zoomBtnEl.style.display = 'none'; bodyEl.innerHTML = `