feat(viewer2d): add complex linetype rendering & LIN pattern parser support

This commit is contained in:
minsung
2026-07-30 17:06:38 +09:00
parent 76f3e20582
commit 1a664f677a
5 changed files with 2214 additions and 206 deletions
+301
View File
@@ -0,0 +1,301 @@
/**
* 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;
}