From 14d1f069277a148988a919be10f7d9872efa438b Mon Sep 17 00:00:00 2001 From: minsung Date: Fri, 31 Jul 2026 09:24:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20DWG=20=EB=82=B4=EC=9E=A5=20=EB=B3=B5?= =?UTF-8?q?=ED=95=A9=20=EB=9D=BC=EC=9D=B8=ED=83=80=EC=9E=85=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EB=B0=94=EC=9D=B8=EB=94=A9=20=EB=B0=8F=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B0=AD=20=EC=A0=95=EB=A0=AC=20=EB=B3=B4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e_lin_test.cjs | 12 ++++-- src/viewer2d/Viewer2D.js | 48 ++++++++++++++------- src/viewer2d/linParser.ts | 89 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 18 deletions(-) diff --git a/e2e_lin_test.cjs b/e2e_lin_test.cjs index 52631d0..a1e1fd5 100644 --- a/e2e_lin_test.cjs +++ b/e2e_lin_test.cjs @@ -30,12 +30,18 @@ async function runE2EDataTest() { } } - console.log('\n4. Verifying Complex Linetype Math in Viewer2D:'); - const { getLinPattern } = require('./src/viewer2d/linParser.ts'); + console.log('\n4. Verifying Complex Linetype Math in Viewer2D with DWG Embedded Data:'); + const { getLinPattern, mergeDwgLinetypePattern } = await import('file:///d:/MYCLAUDE_PROJECT/dwg-dxf-viewer-sample/src/viewer2d/linParser.ts'); const targetTypes = ['H-DICHL06', 'H-DICHL06R', 'H-DICHL02R', 'H-DICHL01R', 'H-DICHL05']; + const dwgLtMap = new Map(); + for (const lt of lineTypes) { + if (lt.name && lt.pattern) dwgLtMap.set(lt.name.toUpperCase(), lt); + } + for (const name of targetTypes) { - const pattern = getLinPattern(name); + const dwgLt = dwgLtMap.get(name); + const pattern = mergeDwgLinetypePattern(name, dwgLt?.pattern || [], dwgLt?.description); if (!pattern) { console.error(` [FAIL] Pattern ${name} not found in registry!`); continue; diff --git a/src/viewer2d/Viewer2D.js b/src/viewer2d/Viewer2D.js index ab1dfa1..b2f503b 100644 --- a/src/viewer2d/Viewer2D.js +++ b/src/viewer2d/Viewer2D.js @@ -11,7 +11,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { aciToHex } from './aciColors.js'; import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces'; import { SlugTextEngine, SlugTextBatch } from './slugText'; -import { getLinPattern } from './linParser'; +import { getLinPattern, mergeDwgLinetypePattern } from './linParser'; const DEFAULT_COLOR = 0xc9d1d9; const ARC_SEGS = 64; @@ -1566,7 +1566,8 @@ export class Viewer2D { } if (!ltName || ltName === 'Continuous' || ltName === 'ByLayer' || ltName === 'ByBlock') return null; - const pattern = getLinPattern(ltName); + const upperKey = ltName.toUpperCase().trim(); + const pattern = this._dwgLinPatterns?.get(upperKey) || getLinPattern(ltName); if (!pattern) return null; const entScale = (e.entityHeader?.linetypeScale > 0 ? e.entityHeader.linetypeScale : 1); @@ -1750,10 +1751,11 @@ export class Viewer2D { } else if (elem.type === 'text') { const pState = getPathState(s); if (elem.text && pendingTexts) { - // 1. Text Height: Exact text style DWG table height if fixed, else linetype elem.scale + // 1. Data-driven Text Height: (fixedH * elem.scale) or (elem.scale * 0.5) to match arrowhead scale ratio const sName = (elem.style || 'STANDARD').toUpperCase(); const fixedH = this._textStyleHeightMap?.get(sName) || 0; - const baseH = fixedH > 0 ? fixedH : (elem.scale || 0.2); + const sFactor = (elem.scale > 0 ? elem.scale * 0.5 : 0.1); + const baseH = fixedH > 0 ? (fixedH * sFactor) : sFactor; const h = baseH * ltscale; let angle = pState.angle + ((elem.rotation || 0) * Math.PI) / 180; @@ -1771,18 +1773,24 @@ export class Viewer2D { const sinT = Math.sin(angle); const xo = (elem.xOffset || 0) * ltscale; - // alignV: 2 (middle) centers text quad on origin; set yo=0 so middle lies directly on grey arrow line axis + // Vertical mid alignment: set yo = 0 so text middle lies directly on line center axis const yo = 0; - // 2. Exact Visual Midpoint: position text at exact midpoint of empty text gap (between arrowhead tip and next dash) - let gapMidOffset = 0; + // 2. Exact Visual Midpoint: determine primary text gap (previous vs next gap) + const prevElem = elements[(elemIdx - 2 + elements.length) % elements.length]; 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 prevGapLen = (prevElem && prevElem.type === 'gap') ? Math.abs(prevElem.val || 0) * ltscale : 0; + const nextGapLen = (nextElem && nextElem.type === 'gap') ? Math.abs(nextElem.val || 0) * ltscale : 0; + + let midState = pState; + if (prevGapLen > nextGapLen && prevGapLen > 1e-6) { + // Text belongs inside previous gap (e.g. H-DICHL06R reverse mode S2 = -0.99) + midState = getPathState(s - prevGapLen / 2); + } else if (nextGapLen > 1e-6) { + // Text belongs inside next gap (e.g. H-DICHL06 forward mode S3 = -0.74) + midState = getPathState(s + nextGapLen / 2); + } const tx = midState.x + (xo * cosT - yo * sinT); const ty = midState.y + (xo * sinT + yo * cosT); @@ -1793,8 +1801,8 @@ export class Viewer2D { height: Math.max(h, 0.01), rotation: angle, color, - alignH: 1, // center horizontally in gap between arrowheads - alignV: 2, // center vertically on line axis + alignH: 1, // center horizontally in gap + alignV: 2, // center vertically on line axis (vertical mid) }); } } @@ -1929,11 +1937,21 @@ export class Viewer2D { // _resolveDash to turn an entity's linetype into a dash/gap size. _buildLinetypeMap(result) { this._ltPatterns = new Map(); + this._dwgLinPatterns = new Map(); this._layerLtByName = new Map(); this._layerLtByHandle = new Map(); this._globalLtscale = (result?.vars?.ltscale > 0 ? result.vars.ltscale : 1); for (const lt of (result?.tables?.lineTypes ?? [])) { - if (lt?.name) this._ltPatterns.set(lt.name, Array.isArray(lt.pattern) ? lt.pattern : []); + if (lt?.name) { + const patArr = Array.isArray(lt.pattern) ? lt.pattern : []; + this._ltPatterns.set(lt.name, patArr); + if (patArr.length > 0) { + const merged = mergeDwgLinetypePattern(lt.name, patArr, lt.description); + if (merged) { + this._dwgLinPatterns.set(lt.name.toUpperCase().trim(), merged); + } + } + } } for (const l of (result?.tables?.layers ?? [])) { const name = l.name ?? l.layerName; diff --git a/src/viewer2d/linParser.ts b/src/viewer2d/linParser.ts index f66cf38..97d7a11 100644 --- a/src/viewer2d/linParser.ts +++ b/src/viewer2d/linParser.ts @@ -299,3 +299,92 @@ export function getLinPattern(name: string): LinPattern | undefined { 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), + }; +} +