feat: enhance complex linetype rendering with DWG text style data, gap midpoint centering, and theme defaults

This commit is contained in:
minsung
2026-07-30 17:59:08 +09:00
parent 1a664f677a
commit 3d3e052f3e
20 changed files with 55456 additions and 16 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -97,7 +97,7 @@
#coords .lbl { color: var(--accent); margin-right: 6px; font-weight: 600; }
#coords .sep { color: var(--line); margin: 0 12px; }
</style>
<script type="module" crossorigin src="/assets/index-DIjyiS5T.js"></script>
<script type="module" crossorigin src="/assets/index-B4PWk4UF.js"></script>
</head>
<body>
<div id="app"></div>
@@ -108,6 +108,7 @@
<input id="file" type="file" accept=".dwg,.dxf" />
</label>
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
<button id="sampleCivil" class="primary wasm" type="button" title="Civil Plan & Profile DWG">평면및종단면도.dwg</button>
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
<button id="fit" type="button">Fit (F)</button>
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
const fs = require('fs');
async function runE2EDataTest() {
console.log('=== STARTING DWG DATA & LINETYPE ACCURACY E2E TEST ===');
const acadrust = await import('./src/viewer2d/acadrust-dwg/acadrust_dwg.js');
const wasmBuffer = fs.readFileSync('./src/viewer2d/acadrust-dwg/acadrust_dwg_bg.wasm');
await acadrust.default({ module_or_path: wasmBuffer });
const dwgFile = './dist/samples/C0060202-001-평면및종단면도(1)(대산방향STA.18+492.08-19+411.50).dwg';
const buf = fs.readFileSync(dwgFile);
const result = acadrust.parse_dwg(new Uint8Array(buf));
console.log('1. Checking DWG Header & Vars:');
console.log(' Global LTSCALE:', result.vars?.ltscale);
console.log('\n2. Checking DWG Text Styles Table:');
const styles = result.tables?.styles || result.tables?.textStyles || [];
console.log(` Found ${styles.length} text styles:`);
for (const st of styles) {
console.log(` - Style [${st.name || st.styleName}]: height=${st.height ?? 0}, font=${st.fontFile || st.font || 'default'}`);
}
console.log('\n3. Checking DWG Linetype Table:');
const lineTypes = result.tables?.lineTypes || [];
console.log(` Found ${lineTypes.length} linetypes in DWG table:`);
for (const lt of lineTypes) {
if (lt.name && (lt.name.includes('DICH') || lt.name.includes('L06') || lt.name.includes('L02'))) {
console.log(` - Linetype [${lt.name}]: desc="${lt.description}", patternLen=${lt.patternLength}, elements=[${lt.pattern?.join(', ')}]`);
}
}
console.log('\n4. Verifying Complex Linetype Math in Viewer2D:');
const { getLinPattern } = require('./src/viewer2d/linParser.ts');
const targetTypes = ['H-DICHL06', 'H-DICHL06R', 'H-DICHL02R', 'H-DICHL01R', 'H-DICHL05'];
for (const name of targetTypes) {
const pattern = getLinPattern(name);
if (!pattern) {
console.error(` [FAIL] Pattern ${name} not found in registry!`);
continue;
}
console.log(`\n * Pattern [${pattern.name}] (patternLength = ${pattern.patternLength}):`);
let cumulativeDist = 0;
for (let i = 0; i < pattern.elements.length; i++) {
const elem = pattern.elements[i];
const nextElem = pattern.elements[(i + 1) % pattern.elements.length];
if (elem.type === 'dash') {
const dLen = elem.val || 0;
console.log(` - [${i}] DASH: len=${dLen} (dist: ${cumulativeDist.toFixed(4)} -> ${(cumulativeDist + dLen).toFixed(4)})`);
cumulativeDist += dLen;
} else if (elem.type === 'gap') {
const gLen = Math.abs(elem.val || 0);
console.log(` - [${i}] GAP: len=${gLen} (dist: ${cumulativeDist.toFixed(4)} -> ${(cumulativeDist + gLen).toFixed(4)})`);
cumulativeDist += gLen;
} else if (elem.type === 'shape') {
console.log(` - [${i}] SHAPE: name=${elem.shapeName}, scale=${elem.scale}, rot=${elem.rotation} at dist=${cumulativeDist.toFixed(4)}`);
} else if (elem.type === 'text') {
const textGapLen = (nextElem && nextElem.type === 'gap') ? Math.abs(nextElem.val || 0) : 0;
const textMidDist = cumulativeDist + textGapLen / 2;
console.log(` - [${i}] TEXT: "${elem.text}", scale=${elem.scale}, style=${elem.style}, yOffset=${elem.yOffset}`);
console.log(` -> Text Gap Start: ${cumulativeDist.toFixed(4)}, Gap End: ${(cumulativeDist + textGapLen).toFixed(4)}`);
console.log(` -> Text EXACT MIDPOINT: ${textMidDist.toFixed(4)} (Midpoint between previous element and next element)`);
}
}
}
console.log('\n=== E2E DATA ACCURACY TEST COMPLETED SUCCESSFULLY ===');
}
runE2EDataTest().catch(err => {
console.error('E2E Test Error:', err);
process.exit(1);
});
+1
View File
@@ -107,6 +107,7 @@
<input id="file" type="file" accept=".dwg,.dxf" />
</label>
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
<button id="sampleCivil" class="primary wasm" type="button" title="Civil Plan & Profile DWG">평면및종단면도.dwg</button>
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
<button id="fit" type="button">Fit (F)</button>
Binary file not shown.
+6 -2
View File
@@ -185,9 +185,14 @@ async function run(label: string, job: () => Promise<CadParseResult>) {
}
}
const civilFile = '/samples/civil.dwg';
document.getElementById('sample')!.addEventListener('click', () => {
void run('BasicSample.dwg', () => loadUrl('/samples/BasicSample.dwg', 'BasicSample.dwg'));
});
document.getElementById('sampleCivil')?.addEventListener('click', () => {
void run('C0060202-001-평면및종단면도.dwg', () => loadUrl(civilFile, 'C0060202-001-평면및종단면도.dwg'));
});
document.getElementById('sampleDxf')!.addEventListener('click', () => {
void run('simple.dxf', () => loadUrl('/samples/simple.dxf', 'simple.dxf'));
});
@@ -197,7 +202,6 @@ document.getElementById('file')!.addEventListener('change', (ev) => {
void run(file.name, () => loadFile(file));
(ev.target as HTMLInputElement).value = '';
});
document.getElementById('fit')!.addEventListener('click', () => viewer.fit());
let dark = true;
document.getElementById('theme')!.addEventListener('click', () => {
dark = !dark;
@@ -216,6 +220,6 @@ window.addEventListener('drop', (e) => {
const model = new URLSearchParams(location.search).get('model');
if (model) void run(model, () => loadUrl(model));
else setStatus('ready · Sample DWG / DXF 또는 파일 드롭');
else void run('C0060202-001-평면및종단면도.dwg', () => loadUrl(civilFile, 'C0060202-001-평면및종단면도.dwg'));
window.addEventListener('resize', () => viewer.resize());
+62 -12
View File
@@ -248,6 +248,7 @@ export class Viewer2D {
this._clear();
this._entityMeta = [];
this._buildLayerMap(result?.tables?.layers);
this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles);
this._buildLinetypeMap(result);
// Sheet orientation: the active model-space viewport's VIEWTWIST rotates the
// view so a drawing stored tilted in WCS (rotated survey sheets — header UCS
@@ -273,6 +274,13 @@ export class Viewer2D {
else this._activeSpaceHex = typeof sh === 'number' ? sh.toString(16) : String(sh).toLowerCase();
}
if (!opts.keepTheme) {
const spacesList = listCadSpaces(result);
const activeSp = spacesList.find((s) => s.handleHex === this._activeSpaceHex);
const isPaper = activeSp?.kind === 'paper';
this.setTheme(!isPaper);
}
const entities = result?.entities || [];
const lineVerts = [];
const lineColors = [];
@@ -1045,9 +1053,9 @@ export class Viewer2D {
// ── UI integration ─────────────────────────────────────────────────────────
/** Swap canvas background for theme. dark=true → near-black, false → light. */
/** Swap canvas background for theme. dark=true → near-black (Model), false → white (Layout). */
setTheme(dark) {
this._scene.background = new THREE.Color(dark ? 0x0a0b0d : 0xf7f6f3);
this._scene.background = new THREE.Color(dark ? 0x0a0b0d : 0xffffff);
if (this._gridVisible) this._rebuildGrid();
}
@@ -1697,12 +1705,25 @@ export class Viewer2D {
};
};
const elements = linPattern.elements || [];
const patternLen = linPattern.patternLength * ltscale;
if (patternLen < 1e-6) return;
if (patternLen < 1e-6 || !elements.length) return;
// Calculate distance from pattern start to shape (arrowhead) element if present
let shapeOffset = -1;
let accum = 0;
for (const e of elements) {
if (e.type === 'shape') {
shapeOffset = accum;
break;
}
if (e.type === 'dash') accum += Math.max((e.val || 0.05) * ltscale, 0.01 * ltscale);
else if (e.type === 'gap') accum += Math.abs(e.val || 0.1) * ltscale;
}
let s = 0;
let elemIdx = 0;
const elements = linPattern.elements;
let lastShapePos = -1;
while (s < totalLen) {
const elem = elements[elemIdx % elements.length];
@@ -1723,12 +1744,18 @@ export class Viewer2D {
const gapLen = Math.abs(elem.val || 0.1) * ltscale;
s += gapLen;
} else if (elem.type === 'shape') {
lastShapePos = s;
const pState = getPathState(s);
this._renderShapeSymbol(elem, pState, ltscale, color, pushSeg);
} else if (elem.type === 'text') {
const pState = getPathState(s);
if (elem.text && pendingTexts) {
const h = (elem.scale || 0.2) * ltscale;
// 1. Text Height: Exact text style DWG table height if fixed, else linetype elem.scale
const sName = (elem.style || 'STANDARD').toUpperCase();
const fixedH = this._textStyleHeightMap?.get(sName) || 0;
const baseH = fixedH > 0 ? fixedH : (elem.scale || 0.2);
const h = baseH * ltscale;
let angle = pState.angle + ((elem.rotation || 0) * Math.PI) / 180;
if (elem.isAbsoluteAngle) {
angle = ((elem.rotation || 0) * Math.PI) / 180;
@@ -1744,20 +1771,30 @@ export class Viewer2D {
const sinT = Math.sin(angle);
const xo = (elem.xOffset || 0) * ltscale;
const rawYo = (elem.yOffset != null && Math.abs(elem.yOffset) > 1e-4) ? Math.abs(elem.yOffset) : 0.02;
const yo = rawYo * ltscale;
// alignV: 2 (middle) centers text quad on origin; set yo=0 so middle lies directly on grey arrow line axis
const yo = 0;
const tx = pState.x + (xo * cosT - yo * sinT);
const ty = pState.y + (xo * sinT + yo * cosT);
// 2. Exact Visual Midpoint: position text at exact midpoint of empty text gap (between arrowhead tip and next dash)
let gapMidOffset = 0;
const nextElem = elements[elemIdx % elements.length];
if (nextElem && nextElem.type === 'gap') {
const gapLen = Math.abs(nextElem.val || 0) * ltscale;
gapMidOffset = gapLen / 2;
}
const midState = gapMidOffset > 1e-6 ? getPathState(s + gapMidOffset) : pState;
const tx = midState.x + (xo * cosT - yo * sinT);
const ty = midState.y + (xo * sinT + yo * cosT);
pendingTexts.push({
text: elem.text,
pos: { x: tx, y: ty, z: pState.z },
pos: { x: tx, y: ty, z: midState.z },
height: Math.max(h, 0.01),
rotation: angle,
color,
alignH: 1,
alignV: 2,
alignH: 1, // center horizontally in gap between arrowheads
alignV: 2, // center vertically on line axis
});
}
}
@@ -1846,6 +1883,19 @@ export class Viewer2D {
}
}
_buildStyleMap(styles) {
if (!this._textStyleHeightMap) this._textStyleHeightMap = new Map();
this._textStyleHeightMap.clear();
if (!styles || !Array.isArray(styles)) return;
for (const st of styles) {
const sName = (st.name || st.styleName || '').toUpperCase();
const h = st.height ?? st.fixedTextHeight ?? 0;
if (sName && h > 0) {
this._textStyleHeightMap.set(sName, h);
}
}
}
_buildLayerMap(layers) {
this._layerByHandle.clear();
this._layerByName.clear();
+1 -1
View File
@@ -328,7 +328,7 @@ export class SlugTextBatch {
colorInt: number, alignH: number, alignV: number): void {
const eng = this.engine;
const lines = String(text).split('\n');
const scale = height / eng.capHeightEm;
const scale = height;
const lineStep = height * 5 / 3; // CAD default MTEXT line spacing
const blockH = (lines.length - 1) * lineStep + height;