/** * LIN File Parser & Complex Linetype Registry * Parses AutoCAD LIN file definitions (including text and shape descriptors). */ import { ICAD_LIN, KOSDIC_LIN } from './linData'; export interface LinElement { type: 'dash' | 'gap' | 'shape' | 'text'; val?: number; // dash length (+) or gap length (-) shapeName?: string; // e.g. KSC35, CIRC1, BOX, TRACK1, BAT, ZIG shxFile?: string; // e.g. kosdic.shx, ltypeshp.shx text?: string; // e.g. "L2", "GAS", "HW" style?: string; // e.g. Standard scale: number; // s=... (default 1.0) rotation: number; // r=... (angle in degrees) isAbsoluteAngle: boolean;// a=... isUprightAngle: boolean; // u=... xOffset: number; // x=... yOffset: number; // y=... } export interface LinPattern { name: string; description: string; elements: LinElement[]; patternLength: number; // total length sum of dashes + abs(gaps) } function parseBracketElement(inner: string): LinElement { const parts: string[] = []; let cur = ''; let inQuote = false; for (let i = 0; i < inner.length; i++) { const c = inner[i]; if (c === '"') inQuote = !inQuote; if (c === ',' && !inQuote) { parts.push(cur.trim()); cur = ''; } else { cur += c; } } if (cur.trim()) parts.push(cur.trim()); let type: 'shape' | 'text' = 'shape'; let shapeName: string | undefined; let shxFile: string | undefined; let text: string | undefined; let style: string | undefined; let scale = 1.0; let rotation = 0; let isAbsoluteAngle = false; let isUprightAngle = false; let xOffset = 0; let yOffset = 0; if (parts.length > 0) { const first = parts[0]; if (first.startsWith('"') && first.endsWith('"')) { type = 'text'; text = first.slice(1, -1); if (parts.length > 1 && !parts[1].includes('=')) style = parts[1]; } else { type = 'shape'; shapeName = first; if (parts.length > 1 && !parts[1].includes('=')) shxFile = parts[1]; } for (let i = 1; i < parts.length; i++) { const part = parts[i]; const eqIdx = part.indexOf('='); if (eqIdx > 0) { const key = part.substring(0, eqIdx).trim().toLowerCase(); const valStr = part.substring(eqIdx + 1).trim(); const val = parseFloat(valStr); if (!isNaN(val)) { if (key === 's') scale = val; else if (key === 'r') { rotation = val; } else if (key === 'a') { rotation = val; isAbsoluteAngle = true; } else if (key === 'u') { rotation = val; isUprightAngle = true; } else if (key === 'x') xOffset = val; else if (key === 'y') yOffset = val; } } } } return { type, shapeName, shxFile, text, style, scale, rotation, isAbsoluteAngle, isUprightAngle, xOffset, yOffset, }; } function tokenizeLinPattern(patternStr: string): string[] { const s = patternStr.replace(/^A\s*,\s*/i, '').trim(); const tokens: string[] = []; let current = ''; let inBracket = false; let inQuote = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (ch === '"') inQuote = !inQuote; if (ch === '[' && !inQuote) inBracket = true; if (ch === ']' && !inQuote) inBracket = false; if (ch === ',' && !inBracket && !inQuote) { if (current.trim()) tokens.push(current.trim()); current = ''; } else { current += ch; } } if (current.trim()) tokens.push(current.trim()); return tokens; } export function parseLinContent(content: string): Map { const result = new Map(); const lines = content.split(/\r?\n/); let currentName: string | null = null; let currentDesc = ''; for (let line of lines) { line = line.trim(); if (!line || line.startsWith(';')) continue; if (line.startsWith('*')) { const commaIdx = line.indexOf(','); if (commaIdx > 0) { currentName = line.substring(1, commaIdx).trim(); currentDesc = line.substring(commaIdx + 1).trim(); } else { currentName = line.substring(1).trim(); currentDesc = ''; } } else if (currentName && line.toUpperCase().startsWith('A,')) { const tokens = tokenizeLinPattern(line); const elements: LinElement[] = []; let totalLength = 0; for (const tok of tokens) { if (tok.startsWith('[') && tok.endsWith(']')) { const elem = parseBracketElement(tok.slice(1, -1)); elements.push(elem); } else { const val = parseFloat(tok); if (!isNaN(val)) { if (val > 1e-6) { elements.push({ type: 'dash', val, scale: 1, rotation: 0, isAbsoluteAngle: false, isUprightAngle: false, xOffset: 0, yOffset: 0, }); totalLength += val; } else if (val < -1e-6) { const absVal = Math.abs(val); elements.push({ type: 'gap', val: -absVal, scale: 1, rotation: 0, isAbsoluteAngle: false, isUprightAngle: false, xOffset: 0, yOffset: 0, }); totalLength += absVal; } else { // Dot (val === 0) elements.push({ type: 'dash', val: 0.05, scale: 1, rotation: 0, isAbsoluteAngle: false, isUprightAngle: false, xOffset: 0, yOffset: 0, }); totalLength += 0.05; } } } } if (elements.length > 0) { result.set(currentName.toUpperCase(), { name: currentName, description: currentDesc, elements, patternLength: Math.max(totalLength, 0.1), }); } currentName = null; } } return result; } // Global Linetype Registry initialized with Icad.lin and KOSDIC.lin const globalRegistry = new Map(); export function registerLinContent(content: string) { const parsed = parseLinContent(content); for (const [key, pattern] of parsed.entries()) { globalRegistry.set(key, pattern); } } // Warm up registry with bundled LIN files registerLinContent(ICAD_LIN); registerLinContent(KOSDIC_LIN); const CIVIL_LT_TEXT_MAP: Record = { 'H-DICHL02': 'L2-2/0', 'H-DICHL02R': 'L2-2', 'H-DICHL01': 'L2-3/0', 'H-DICHL01R': 'L2-3', 'H-DICHL06': 'L5-1', 'H-DICHL06R': 'L5-1', 'H-DICHL05': 'L5', 'H-DICHL05R': 'L5', 'H-DICHU05': 'U5', 'H-DICHU05R': 'U5', 'H-DICHB01': 'B1', 'H-DICHB01R': 'B1', 'H-DICHM03': 'M3', 'H-DICHM03R': 'M3', 'H-DICHV01': 'V1', 'H-DICHV01R': 'V1', 'H-DICHO01': 'O1', 'H-DICHO01R': 'O1', 'H-DICHL03': 'L3', 'H-DICHL03R': 'L3', 'H-DICHL04': 'L4', 'H-DICHL04R': 'L4', 'H-DICHL08': 'L8', 'H-DICHL08R': 'L8', }; const NO_SHAPE_LINETYPES = new Set([ 'H-DICHL05', 'H-DICHL05R', 'H-DICHU05', 'H-DICHU05R', 'H-DICHB01', 'H-DICHB01R', 'H-DICHM03', 'H-DICHM03R', 'H-DICHV01', 'H-DICHV01R', 'H-DICHO01', 'H-DICHO01R', ]); export function getLinPattern(name: string): LinPattern | undefined { if (!name) return undefined; const key = name.toUpperCase().trim(); const pattern = globalRegistry.get(key); if (!pattern) return undefined; let elements = pattern.elements; // Filter out shape/arrowhead elements for L5, U5, B1, etc. as specified in official CAD spec if (NO_SHAPE_LINETYPES.has(key)) { elements = elements.filter((elem) => elem.type !== 'shape'); } const overrideText = CIVIL_LT_TEXT_MAP[key]; if (overrideText) { elements = elements.map((elem) => elem.type === 'text' ? { ...elem, text: overrideText } : elem ); } return { ...pattern, elements, }; } export function getAllRegisteredPatterns(): Map { return globalRegistry; } export function mergeDwgLinetypePattern( ltName: string, dwgPattern: number[], description?: string ): LinPattern | undefined { if (!ltName) return undefined; const base = getLinPattern(ltName); if (!dwgPattern || !dwgPattern.length) return base; if (!base) { const elements: LinElement[] = []; let totalLength = 0; for (const val of dwgPattern) { if (val > 1e-6) { elements.push({ type: 'dash', val, scale: 1, rotation: 0, isAbsoluteAngle: false, isUprightAngle: false, xOffset: 0, yOffset: 0, }); totalLength += val; } else if (val < -1e-6) { const absVal = Math.abs(val); elements.push({ type: 'gap', val: -absVal, scale: 1, rotation: 0, isAbsoluteAngle: false, isUprightAngle: false, xOffset: 0, yOffset: 0, }); totalLength += absVal; } } return { name: ltName, description: description || '', elements, patternLength: Math.max(totalLength, 0.1), }; } // Merge DWG numeric dash/gap values into base pattern elements const newElements: LinElement[] = []; let dwgValIdx = 0; let totalLength = 0; for (const elem of base.elements) { if (elem.type === 'dash') { const dwgVal = dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] > 0 ? dwgPattern[dwgValIdx] : elem.val || 0.05; if (dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] > 0) { dwgValIdx++; } newElements.push({ ...elem, val: dwgVal }); totalLength += dwgVal; } else if (elem.type === 'gap') { const dwgVal = dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] < 0 ? dwgPattern[dwgValIdx] : elem.val; if (dwgValIdx < dwgPattern.length && dwgPattern[dwgValIdx] < 0) { dwgValIdx++; } const absVal = Math.abs(dwgVal || 0.1); newElements.push({ ...elem, val: -absVal }); totalLength += absVal; } else { newElements.push({ ...elem }); } } return { ...base, description: description || base.description, elements: newElements, patternLength: Math.max(totalLength, 0.1), }; }