import { Box2 } from 'vecks' import colors from './util/colors' import escapeXmlText from './util/escapeXmlText' import round10 from './util/round10' import type { DimensionEntity, ToSVGOptions } from './types' import type { DimStyleTable } from './types/dxf' import type { BoundsAndElement } from './types/svg' const DEFAULT_DIMENSION_DECIMALS = 2 export interface DimensionViewport { width: number height: number } // AutoScale is meant to improve readability of dimension graphics. // Scale is derived from the drawing viewport (final SVG viewBox size). // No min/max clamp by design. const AUTOSCALE_VIEWPORT_REFERENCE = 40 const computeViewportAutoScaleFactor = ( viewport: DimensionViewport, options: ToSVGOptions | undefined, ): number => { const viewportMin = Math.min(Math.abs(viewport.width), Math.abs(viewport.height)) if (!Number.isFinite(viewportMin) || viewportMin <= 0) return 1 const reference = options?.dimension?.autoScaleViewportReference const safeReference = Number.isFinite(reference) && (reference ?? 0) > 0 ? (reference as number) : AUTOSCALE_VIEWPORT_REFERENCE return viewportMin / safeReference } const getViewportMin = (viewport: DimensionViewport): number => { const viewportMin = Math.min(Math.abs(viewport.width), Math.abs(viewport.height)) return Number.isFinite(viewportMin) ? viewportMin : Number.NaN } const getViewportPercentageSize = ( viewport: DimensionViewport, percent: number | undefined, ): number | undefined => { if (!Number.isFinite(percent) || (percent ?? 0) <= 0) return undefined const viewportMin = getViewportMin(viewport) if (!Number.isFinite(viewportMin) || viewportMin <= 0) return undefined return viewportMin * ((percent as number) / 100) } export const getDimensionGeometryBBox = (entity: DimensionEntity): Box2 => { const bbox = new Box2() const points = [ entity.start, entity.angleVertex, entity.arcPoint, entity.textMidpoint, entity.measureStart, entity.measureEnd, ] for (const p of points) { if (!p) continue const x = p.x const y = p.y if (!Number.isFinite(x) || !Number.isFinite(y)) continue bbox.expandByPoint({ x, y }) } return bbox } const getScaledDimensionSizes = ( dimStyle: DimStyleTable | undefined, options: ToSVGOptions | undefined, viewport: DimensionViewport | undefined, ): { arrowSize: number textHeight: number extLineOffset: number extLineExtension: number } => { const autoScale = options?.dimension?.autoScale === true const baseArrowSize = dimStyle?.dimAsz ?? 2.5 const baseTextHeight = dimStyle?.dimTxt ?? 2.5 const baseExtLineOffset = dimStyle?.dimExo ?? 0.625 const baseExtLineExtension = dimStyle?.dimExe ?? 1.25 if (!autoScale || !viewport) { return { arrowSize: baseArrowSize, textHeight: baseTextHeight, extLineOffset: baseExtLineOffset, extLineExtension: baseExtLineExtension, } } const scale = computeViewportAutoScaleFactor(viewport, options) const perc = options?.dimension?.autoScaleViewportPercentages const arrowFromPct = getViewportPercentageSize(viewport, perc?.arrowSize) const textFromPct = getViewportPercentageSize(viewport, perc?.textHeight) const offsetFromPct = getViewportPercentageSize(viewport, perc?.extLineOffset) const extensionFromPct = getViewportPercentageSize(viewport, perc?.extLineExtension) return { arrowSize: arrowFromPct ?? (baseArrowSize * scale), textHeight: textFromPct ?? (baseTextHeight * scale), extLineOffset: offsetFromPct ?? (baseExtLineOffset * scale), extLineExtension: extensionFromPct ?? (baseExtLineExtension * scale), } } const formatDimensionValue = ( value: number, decimals: number = DEFAULT_DIMENSION_DECIMALS, ): string => { if (!Number.isFinite(value)) return '' const rounded = round10(value, -decimals) return rounded.toFixed(decimals) } const computeRadiusFallback = (entity: DimensionEntity): number => { const cx = entity.start?.x ?? 0 const cy = entity.start?.y ?? 0 const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 const r1 = Math.hypot(x1 - cx, y1 - cy) const r2 = Math.hypot(x2 - cx, y2 - cy) const chord = Math.hypot(x2 - x1, y2 - y1) return Math.max(r1, r2, chord) } const computeLinearDistance = ( x1: number, y1: number, x2: number, y2: number, ): number => Math.hypot(x2 - x1, y2 - y1) const computeAngularDegreesMinimal = ( cx: number, cy: number, x1: number, y1: number, x2: number, y2: number, ): number => { const a1 = Math.atan2(y1 - cy, x1 - cx) const a2 = Math.atan2(y2 - cy, x2 - cx) let delta = Math.abs(a2 - a1) while (delta > Math.PI * 2) delta -= Math.PI * 2 if (delta > Math.PI) delta = Math.PI * 2 - delta return (delta * 180) / Math.PI } const computeAngularDegreesCCW = ( cx: number, cy: number, x1: number, y1: number, x2: number, y2: number, ): number => { const a1 = Math.atan2(y1 - cy, x1 - cx) const a2 = Math.atan2(y2 - cy, x2 - cx) let delta = a2 - a1 while (delta < 0) delta += Math.PI * 2 while (delta >= Math.PI * 2) delta -= Math.PI * 2 return (delta * 180) / Math.PI } const computeDimensionMeasurement = (entity: DimensionEntity): string => { const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 switch (entity.dimensionType) { case 0: case 1: case 6: { const dist = computeLinearDistance(x1, y1, x2, y2) return formatDimensionValue(dist) } case 3: { const dist = computeLinearDistance(x1, y1, x2, y2) if (dist > 0) return formatDimensionValue(dist) const radius = computeRadiusFallback(entity) return formatDimensionValue(radius * 2) } case 4: { const dist = computeLinearDistance(x1, y1, x2, y2) if (dist > 0) return formatDimensionValue(dist) const radius = computeRadiusFallback(entity) return formatDimensionValue(radius) } case 2: { const cx = entity.start?.x ?? 0 const cy = entity.start?.y ?? 0 const degrees = computeAngularDegreesMinimal(cx, cy, x1, y1, x2, y2) const formatted = formatDimensionValue(degrees) return formatted ? `${formatted}°` : '' } case 5: { const cx = entity.angleVertex?.x ?? 0 const cy = entity.angleVertex?.y ?? 0 const degrees = computeAngularDegreesCCW(cx, cy, x1, y1, x2, y2) const formatted = formatDimensionValue(degrees) return formatted ? `${formatted}°` : '' } default: return '' } } const resolveDimensionText = (entity: DimensionEntity): string => { const raw = typeof entity.text === 'string' ? entity.text : '' const trimmed = raw.trim() const measured = computeDimensionMeasurement(entity) if (!trimmed) return measured if (trimmed.includes('<>')) { return trimmed.split('<>').join(measured) } return trimmed } const expandBBoxForMarker = (bbox: Box2, x: number, y: number, size: number) => { bbox.expandByPoint({ x: x - size, y: y - size }) bbox.expandByPoint({ x: x + size, y: y + size }) } const expandBBoxForText = ( bbox: Box2, x: number, y: number, height: number, content: string, ) => { const textWidth = content.length * height * 0.6 // text-anchor="middle" is used everywhere in DIMENSION rendering bbox.expandByPoint({ x: x - textWidth / 2, y: y - height }) bbox.expandByPoint({ x: x + textWidth / 2, y: y + height }) } /** * Convert DXF color number to SVG color string */ function colorNumberToSVG(colorNumber?: number): string { if (colorNumber === undefined || colorNumber < 0) { return 'currentColor' } // DXF color 0 is ByBlock, 256 is ByLayer, 7 is white/black (depends on bg) if (colorNumber === 0 || colorNumber === 256) { return 'currentColor' } // Get RGB from color table const rgb = colors[colorNumber] if (!rgb) { return 'currentColor' } return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` } /** * Get dimension colors and weights from DIMSTYLE with defaults */ function getDimensionColors(dimStyle?: DimStyleTable): { dimLineColor: string extLineColor: string textColor: string dimLineWeight: number extLineWeight: number } { return { dimLineColor: colorNumberToSVG(dimStyle?.dimClrd), extLineColor: colorNumberToSVG(dimStyle?.dimClre), textColor: colorNumberToSVG(dimStyle?.dimClrt), dimLineWeight: dimStyle?.dimLwd ?? 0.5, extLineWeight: dimStyle?.dimLwe ?? 0.5, } } /** * Render DIMENSION entity to SVG with proper DIMSTYLE support */ export default function dimensionToSVG( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { // Dispatch to appropriate renderer based on dimension type switch (entity.dimensionType) { case 0: // Rotated, horizontal, or vertical case 1: // Aligned return renderLinearDimension(entity, dimStyle, options, viewport) case 2: // Angular return renderAngularDimension(entity, dimStyle, options, viewport) case 5: // Angular 3-point return renderAngular3PointDimension(entity, dimStyle, options, viewport) case 3: // Diameter return renderDiameterDimension(entity, dimStyle, options, viewport) case 4: // Radius return renderRadialDimension(entity, dimStyle, options, viewport) case 6: // Ordinate return renderOrdinateDimension(entity, dimStyle, options, viewport) default: // Fallback to simple line rendering return renderFallbackDimension(entity) } } /** * Render angular 3-point dimension (type 5). * * Based on DXF reference + ezdxf: angle is measured from p1 to p2 * counter-clockwise around the vertex. */ function renderAngular3PointDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] const markers: string[] = [] const { arrowSize, textHeight } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, extLineColor, textColor, dimLineWeight, extLineWeight } = getDimensionColors(dimStyle) const vertexX = entity.angleVertex?.x ?? 0 const vertexY = entity.angleVertex?.y ?? 0 const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 // DXF reference: point (10,20,30) specifies the dimension line arc location. // In practice, ezdxf may also provide (16,26,36); prefer arcPoint only if it // yields a meaningful radius away from the vertex. const startArcX = entity.start?.x ?? 0 const startArcY = entity.start?.y ?? 0 const arcPointX = entity.arcPoint?.x const arcPointY = entity.arcPoint?.y const arcPointRadius = Number.isFinite(arcPointX) && Number.isFinite(arcPointY) ? Math.hypot((arcPointX as number) - vertexX, (arcPointY as number) - vertexY) : Number.NaN const useArcPoint = Number.isFinite(arcPointRadius) && arcPointRadius > 1e-9 const arcLocationX = useArcPoint ? (arcPointX as number) : startArcX const arcLocationY = useArcPoint ? (arcPointY as number) : startArcY const textX = entity.textMidpoint?.x ?? arcLocationX const textY = entity.textMidpoint?.y ?? arcLocationY bbox.expandByPoint({ x: vertexX, y: vertexY }) bbox.expandByPoint({ x: x1, y: y1 }) bbox.expandByPoint({ x: x2, y: y2 }) bbox.expandByPoint({ x: arcLocationX, y: arcLocationY }) bbox.expandByPoint({ x: textX, y: textY }) const a1 = Math.atan2(y1 - vertexY, x1 - vertexX) const a2 = Math.atan2(y2 - vertexY, x2 - vertexX) let radius = Math.hypot(arcLocationX - vertexX, arcLocationY - vertexY) if (!Number.isFinite(radius) || radius <= 1e-9) { radius = Math.hypot(textX - vertexX, textY - vertexY) } if (!Number.isFinite(radius) || radius <= 1e-9) { radius = Math.max( Math.hypot(x1 - vertexX, y1 - vertexY), Math.hypot(x2 - vertexX, y2 - vertexY), ) } const arcStartX = vertexX + radius * Math.cos(a1) const arcStartY = vertexY + radius * Math.sin(a1) const arcEndX = vertexX + radius * Math.cos(a2) const arcEndY = vertexY + radius * Math.sin(a2) bbox.expandByPoint({ x: arcStartX, y: arcStartY }) bbox.expandByPoint({ x: arcEndX, y: arcEndY }) // Create arrow markers const markerId1 = `dim-angular-3p-arrow-start-${Date.now()}` const markerId2 = `dim-angular-3p-arrow-end-${Date.now()}` markers.push( createArrowMarker(markerId1, arrowSize, dimLineColor, 'backward'), createArrowMarker(markerId2, arrowSize, dimLineColor, 'forward'), ) // Extension lines from definition points to arc endpoints. elements.push( ``, ``, ) // Arc from a1 to a2 in CCW orientation. let delta = a2 - a1 while (delta < 0) delta += Math.PI * 2 while (delta >= Math.PI * 2) delta -= Math.PI * 2 const largeArcFlag = delta > Math.PI ? 1 : 0 const sweepFlag = 1 expandBBoxForMarker(bbox, arcStartX, arcStartY, arrowSize) expandBBoxForMarker(bbox, arcEndX, arcEndY, arrowSize) elements.push( ``, ) const resolvedText = resolveDimensionText(entity) if (resolvedText) { const midAngle = a1 + delta / 2 const textRotation = (midAngle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, resolvedText) elements.push( `${escapeXmlText(resolvedText)}`, ) } return { bbox, element: `${markers.join('')}${elements.join('')}`, } } /** * Create SVG marker definition for dimension arrows */ export function createArrowMarker( id: string, size: number, color: string, direction: 'forward' | 'backward' = 'forward', ): string { const arrowPath = direction === 'forward' ? `M 0 0 L ${size} ${size / 2} L 0 ${size} z` : `M ${size} 0 L 0 ${size / 2} L ${size} ${size} z` const refX = direction === 'forward' ? size : 0 return ` ` } /** * Render linear dimension (rotated, horizontal, vertical, or aligned) */ function renderLinearDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] const markers: string[] = [] // Get dimension style properties with defaults (optionally auto-scaled) const { arrowSize, textHeight, extLineOffset, extLineExtension } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, extLineColor, textColor, dimLineWeight, extLineWeight } = getDimensionColors(dimStyle) // Extract dimension geometry const defPoint1X = entity.measureStart?.x ?? 0 const defPoint1Y = entity.measureStart?.y ?? 0 const defPoint2X = entity.measureEnd?.x ?? 0 const defPoint2Y = entity.measureEnd?.y ?? 0 const dimLineY = entity.start?.y ?? 0 const textX = entity.textMidpoint?.x ?? (defPoint1X + defPoint2X) / 2 const textY = entity.textMidpoint?.y ?? (defPoint1Y + defPoint2Y) / 2 // Calculate dimension line angle const angle = Math.atan2(defPoint2Y - defPoint1Y, defPoint2X - defPoint1X) const perpAngle = angle + Math.PI / 2 // Calculate dimension line endpoints const dimLine1X = defPoint1X + Math.cos(perpAngle) * (dimLineY - defPoint1Y) const dimLine1Y = defPoint1Y + Math.sin(perpAngle) * (dimLineY - defPoint1Y) const dimLine2X = defPoint2X + Math.cos(perpAngle) * (dimLineY - defPoint2Y) const dimLine2Y = defPoint2Y + Math.sin(perpAngle) * (dimLineY - defPoint2Y) // Expand bounding box bbox.expandByPoint({ x: defPoint1X, y: defPoint1Y }) bbox.expandByPoint({ x: defPoint2X, y: defPoint2Y }) bbox.expandByPoint({ x: dimLine1X, y: dimLine1Y }) bbox.expandByPoint({ x: dimLine2X, y: dimLine2Y }) bbox.expandByPoint({ x: textX, y: textY }) // Create unique marker IDs for arrows const markerId1 = `dim-arrow-start-${Date.now()}` const markerId2 = `dim-arrow-end-${Date.now()}` // Create arrow markers with dimension line color markers.push( createArrowMarker(markerId1, arrowSize, dimLineColor, 'backward'), createArrowMarker(markerId2, arrowSize, dimLineColor, 'forward'), ) // Draw extension lines const extLine1StartX = defPoint1X + Math.cos(perpAngle) * extLineOffset const extLine1StartY = defPoint1Y + Math.sin(perpAngle) * extLineOffset const extLine1EndX = dimLine1X + Math.cos(perpAngle) * extLineExtension const extLine1EndY = dimLine1Y + Math.sin(perpAngle) * extLineExtension const extLine2StartX = defPoint2X + Math.cos(perpAngle) * extLineOffset const extLine2StartY = defPoint2Y + Math.sin(perpAngle) * extLineOffset const extLine2EndX = dimLine2X + Math.cos(perpAngle) * extLineExtension const extLine2EndY = dimLine2Y + Math.sin(perpAngle) * extLineExtension // Expand bounding box to include full extension lines and arrow markers bbox.expandByPoint({ x: extLine1StartX, y: extLine1StartY }) bbox.expandByPoint({ x: extLine1EndX, y: extLine1EndY }) bbox.expandByPoint({ x: extLine2StartX, y: extLine2StartY }) bbox.expandByPoint({ x: extLine2EndX, y: extLine2EndY }) expandBBoxForMarker(bbox, dimLine1X, dimLine1Y, arrowSize) expandBBoxForMarker(bbox, dimLine2X, dimLine2Y, arrowSize) elements.push( ``, ``, ``, ) // Add dimension text const resolvedText = resolveDimensionText(entity) if (resolvedText) { const textRotation = (angle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, resolvedText) elements.push( `${escapeXmlText(resolvedText)}`, ) } return { bbox, element: `${markers.join('')}${elements.join('')}`, } } /** * Render angular dimension */ function renderAngularDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] const markers: string[] = [] // Get dimension style properties (optionally auto-scaled) const { arrowSize, textHeight } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, extLineColor, textColor, dimLineWeight, extLineWeight } = getDimensionColors(dimStyle) // Extract points const centerX = entity.start?.x ?? 0 const centerY = entity.start?.y ?? 0 const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 const textX = entity.textMidpoint?.x ?? centerX const textY = entity.textMidpoint?.y ?? centerY bbox.expandByPoint({ x: centerX, y: centerY }) bbox.expandByPoint({ x: x1, y: y1 }) bbox.expandByPoint({ x: x2, y: y2 }) bbox.expandByPoint({ x: textX, y: textY }) // Create arrow markers const markerId1 = `dim-angular-arrow-start-${Date.now()}` const markerId2 = `dim-angular-arrow-end-${Date.now()}` markers.push( createArrowMarker(markerId1, arrowSize, dimLineColor, 'backward'), createArrowMarker(markerId2, arrowSize, dimLineColor, 'forward'), ) // Draw extension lines from center to definition points elements.push( ``, ``, ) // Calculate arc radius (distance from center to text midpoint) const radius = Math.hypot(textX - centerX, textY - centerY) const startAngle = Math.atan2(y1 - centerY, x1 - centerX) const endAngle = Math.atan2(y2 - centerY, x2 - centerX) // Draw arc for angular dimension const largeArcFlag = Math.abs(endAngle - startAngle) > Math.PI ? 1 : 0 const arcStartX = centerX + radius * Math.cos(startAngle) const arcStartY = centerY + radius * Math.sin(startAngle) const arcEndX = centerX + radius * Math.cos(endAngle) const arcEndY = centerY + radius * Math.sin(endAngle) elements.push( ``, ) // Add dimension text const resolvedText = resolveDimensionText(entity) if (resolvedText) { const midAngle = (startAngle + endAngle) / 2 const textRotation = (midAngle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, resolvedText) elements.push( `${escapeXmlText(resolvedText)}`, ) } return { bbox, element: `${markers.join('')}${elements.join('')}`, } } /** * Render diameter dimension */ function renderDiameterDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] const markers: string[] = [] // Get dimension style properties (optionally auto-scaled) const { arrowSize, textHeight } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, textColor, dimLineWeight } = getDimensionColors(dimStyle) // Extract geometry const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 const textX = entity.textMidpoint?.x ?? (x1 + x2) / 2 const textY = entity.textMidpoint?.y ?? (y1 + y2) / 2 bbox.expandByPoint({ x: x1, y: y1 }) bbox.expandByPoint({ x: x2, y: y2 }) bbox.expandByPoint({ x: textX, y: textY }) const diameterLen = Math.hypot(x2 - x1, y2 - y1) if (Number.isFinite(diameterLen) && diameterLen > 1e-6) { // Create arrow markers const markerId = `dim-diameter-arrow-${Date.now()}` markers.push(createArrowMarker(markerId, arrowSize, dimLineColor, 'backward')) // Create diameter line with arrow at the end elements.push( ``, ) expandBBoxForMarker(bbox, x2, y2, arrowSize) } // Add dimension text with diameter symbol const resolvedText = resolveDimensionText(entity) const diameterText = resolvedText ? `⌀${resolvedText}` : '⌀' const angle = Math.atan2(y2 - y1, x2 - x1) const textRotation = (angle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, diameterText) elements.push( `${escapeXmlText(diameterText)}`, ) return { bbox, element: `${markers.join('')}${elements.join('')}`, } } /** * Render radial dimension */ function renderRadialDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] const markers: string[] = [] // Get dimension style properties (optionally auto-scaled) const { arrowSize, textHeight } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, textColor, dimLineWeight } = getDimensionColors(dimStyle) // Extract geometry const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.measureEnd?.x ?? 0 const y2 = entity.measureEnd?.y ?? 0 const textX = entity.textMidpoint?.x ?? (x1 + x2) / 2 const textY = entity.textMidpoint?.y ?? (y1 + y2) / 2 bbox.expandByPoint({ x: x1, y: y1 }) bbox.expandByPoint({ x: x2, y: y2 }) bbox.expandByPoint({ x: textX, y: textY }) const radiusLen = Math.hypot(x2 - x1, y2 - y1) if (Number.isFinite(radiusLen) && radiusLen > 1e-6) { // Create arrow markers const markerId = `dim-radius-arrow-${Date.now()}` markers.push(createArrowMarker(markerId, arrowSize, dimLineColor, 'backward')) // Create radius line with arrow at the end elements.push( ``, ) expandBBoxForMarker(bbox, x2, y2, arrowSize) } // Add dimension text with radius symbol const resolvedText = resolveDimensionText(entity) const radiusText = resolvedText ? `R${resolvedText}` : 'R' const angle = Math.atan2(y2 - y1, x2 - x1) const textRotation = (angle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, radiusText) elements.push( `${escapeXmlText(radiusText)}`, ) return { bbox, element: `${markers.join('')}${elements.join('')}`, } } /** * Render ordinate dimension */ function renderOrdinateDimension( entity: DimensionEntity, dimStyle?: DimStyleTable, options?: ToSVGOptions, viewport?: DimensionViewport, ): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] // Get dimension style properties (optionally auto-scaled) const { textHeight } = getScaledDimensionSizes(dimStyle, options, viewport) const { dimLineColor, textColor, dimLineWeight } = getDimensionColors(dimStyle) // Extract geometry const x1 = entity.measureStart?.x ?? 0 const y1 = entity.measureStart?.y ?? 0 const x2 = entity.start?.x ?? 0 const y2 = entity.start?.y ?? 0 const textX = entity.textMidpoint?.x ?? x2 const textY = entity.textMidpoint?.y ?? y2 bbox.expandByPoint({ x: x1, y: y1 }) bbox.expandByPoint({ x: x2, y: y2 }) bbox.expandByPoint({ x: textX, y: textY }) // Create leader line (no arrow for ordinate dimensions) elements.push( ``, ) // Add dimension text const resolvedText = resolveDimensionText(entity) if (resolvedText) { const angle = Math.atan2(y2 - y1, x2 - x1) const textRotation = (angle * 180) / Math.PI expandBBoxForText(bbox, textX, textY, textHeight, resolvedText) elements.push( `${escapeXmlText(resolvedText)}`, ) } return { bbox, element: `${elements.join('')}`, } } /** * Fallback renderer for unsupported dimension types */ function renderFallbackDimension(entity: DimensionEntity): BoundsAndElement { const bbox = new Box2() const elements: string[] = [] // Just render text at midpoint if (entity.textMidpoint) { const textX = entity.textMidpoint.x ?? 0 const textY = entity.textMidpoint.y ?? 0 bbox.expandByPoint({ x: textX, y: textY }) const resolvedText = resolveDimensionText(entity) if (resolvedText) { elements.push( `${escapeXmlText(resolvedText)}`, ) } } return { bbox, element: `${elements.join('')}`, } }