391 lines
10 KiB
TypeScript
391 lines
10 KiB
TypeScript
/**
|
|
* 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<string, LinPattern> {
|
|
const result = new Map<string, LinPattern>();
|
|
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<string, LinPattern>();
|
|
|
|
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<string, string> = {
|
|
'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<string, LinPattern> {
|
|
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),
|
|
};
|
|
}
|
|
|