@linkiez/dxf-renew 5.3.1 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/{docs/CODE_PATTERNS.md → .github/instructions/code-patterns.instructions.md} +4 -1
  2. package/.github/instructions/exdxf.instruction.md +161 -0
  3. package/.github/instructions/tdd.instructions.md +271 -0
  4. package/.github/workflows/release.yml +4 -5
  5. package/.releaserc.json +1 -1
  6. package/.yarn/install-state.gz +0 -0
  7. package/CHANGELOG.md +59 -0
  8. package/CONTRIBUTING.md +16 -14
  9. package/PLAN.md +34 -84
  10. package/README.md +43 -8
  11. package/dist/dxf.js +1388 -376
  12. package/docs/DIMENSION_SUMMARY.md +11 -5
  13. package/docs/DXF_VERSION_SUPPORT.md +45 -0
  14. package/docs/ENTITY_SVG_ROADMAP.md +96 -0
  15. package/docs/EZDXF_REFERENCE_SITEMAP.md +55 -0
  16. package/docs/FIXTURE_VALIDATION_EZDXF.md +62 -0
  17. package/docs/README.md +22 -0
  18. package/docs/SVG_RENDERING_INTEGRATION_TESTS.md +119 -0
  19. package/docs/TEXT-MTEXT-DIMENSION-SUPPORT.md +1 -1
  20. package/lib/Helper.cjs +2 -2
  21. package/lib/Helper.cjs.map +2 -2
  22. package/lib/Helper.js +2 -2
  23. package/lib/Helper.js.map +2 -2
  24. package/lib/denormalise.cjs +131 -91
  25. package/lib/denormalise.cjs.map +2 -2
  26. package/lib/denormalise.js +131 -91
  27. package/lib/denormalise.js.map +2 -2
  28. package/lib/dimensionToSVG.cjs +318 -53
  29. package/lib/dimensionToSVG.cjs.map +3 -3
  30. package/lib/dimensionToSVG.js +316 -52
  31. package/lib/dimensionToSVG.js.map +2 -2
  32. package/lib/handlers/entities.cjs +90 -26
  33. package/lib/handlers/entities.cjs.map +3 -3
  34. package/lib/handlers/entities.js +90 -26
  35. package/lib/handlers/entities.js.map +3 -3
  36. package/lib/handlers/entity/dgnUnderlay.cjs +106 -0
  37. package/lib/handlers/entity/dgnUnderlay.cjs.map +7 -0
  38. package/lib/handlers/entity/dgnUnderlay.js +71 -0
  39. package/lib/handlers/entity/dgnUnderlay.js.map +7 -0
  40. package/lib/handlers/entity/dimension.cjs +24 -0
  41. package/lib/handlers/entity/dimension.cjs.map +2 -2
  42. package/lib/handlers/entity/dimension.js +24 -0
  43. package/lib/handlers/entity/dimension.js.map +2 -2
  44. package/lib/handlers/entity/dwfUnderlay.cjs +106 -0
  45. package/lib/handlers/entity/dwfUnderlay.cjs.map +7 -0
  46. package/lib/handlers/entity/dwfUnderlay.js +71 -0
  47. package/lib/handlers/entity/dwfUnderlay.js.map +7 -0
  48. package/lib/handlers/entity/image.cjs +123 -0
  49. package/lib/handlers/entity/image.cjs.map +7 -0
  50. package/lib/handlers/entity/image.js +88 -0
  51. package/lib/handlers/entity/image.js.map +7 -0
  52. package/lib/handlers/entity/leader.cjs +148 -0
  53. package/lib/handlers/entity/leader.cjs.map +7 -0
  54. package/lib/handlers/entity/leader.js +113 -0
  55. package/lib/handlers/entity/leader.js.map +7 -0
  56. package/lib/handlers/entity/pdfUnderlay.cjs +106 -0
  57. package/lib/handlers/entity/pdfUnderlay.cjs.map +7 -0
  58. package/lib/handlers/entity/pdfUnderlay.js +71 -0
  59. package/lib/handlers/entity/pdfUnderlay.js.map +7 -0
  60. package/lib/handlers/entity/tolerance.cjs +90 -0
  61. package/lib/handlers/entity/tolerance.cjs.map +7 -0
  62. package/lib/handlers/entity/tolerance.js +55 -0
  63. package/lib/handlers/entity/tolerance.js.map +7 -0
  64. package/lib/handlers/objects.cjs +257 -136
  65. package/lib/handlers/objects.cjs.map +2 -2
  66. package/lib/handlers/objects.js +257 -136
  67. package/lib/handlers/objects.js.map +2 -2
  68. package/lib/toSVG.cjs +71 -8
  69. package/lib/toSVG.cjs.map +3 -3
  70. package/lib/toSVG.js +72 -9
  71. package/lib/toSVG.js.map +2 -2
  72. package/lib/types/dimension-entity.cjs.map +1 -1
  73. package/lib/types/entity.cjs.map +1 -1
  74. package/lib/types/image-entity.cjs +17 -0
  75. package/lib/types/image-entity.cjs.map +7 -0
  76. package/lib/types/image-entity.js +1 -0
  77. package/lib/types/image-entity.js.map +7 -0
  78. package/lib/types/index.cjs +8 -0
  79. package/lib/types/index.cjs.map +2 -2
  80. package/lib/types/index.js +4 -0
  81. package/lib/types/index.js.map +2 -2
  82. package/lib/types/leader-entity.cjs +17 -0
  83. package/lib/types/leader-entity.cjs.map +7 -0
  84. package/lib/types/leader-entity.js +1 -0
  85. package/lib/types/leader-entity.js.map +7 -0
  86. package/lib/types/options.cjs.map +1 -1
  87. package/lib/types/tables.cjs.map +1 -1
  88. package/lib/types/tolerance-entity.cjs +17 -0
  89. package/lib/types/tolerance-entity.cjs.map +7 -0
  90. package/lib/types/tolerance-entity.js +1 -0
  91. package/lib/types/tolerance-entity.js.map +7 -0
  92. package/lib/types/underlay-entity.cjs +17 -0
  93. package/lib/types/underlay-entity.cjs.map +7 -0
  94. package/lib/types/underlay-entity.js +1 -0
  95. package/lib/types/underlay-entity.js.map +7 -0
  96. package/lib/util/escapeXmlText.cjs +27 -0
  97. package/lib/util/escapeXmlText.cjs.map +7 -0
  98. package/lib/util/escapeXmlText.js +7 -0
  99. package/lib/util/escapeXmlText.js.map +7 -0
  100. package/package.json +13 -4
  101. package/playwright.config.cjs +20 -0
  102. package/src/Helper.ts +3 -3
  103. package/src/denormalise.ts +182 -116
  104. package/src/dimensionToSVG.ts +466 -54
  105. package/src/handlers/entities.ts +109 -34
  106. package/src/handlers/entity/dgnUnderlay.ts +94 -0
  107. package/src/handlers/entity/dimension.ts +27 -1
  108. package/src/handlers/entity/dwfUnderlay.ts +94 -0
  109. package/src/handlers/entity/image.ts +118 -0
  110. package/src/handlers/entity/leader.ts +153 -0
  111. package/src/handlers/entity/pdfUnderlay.ts +94 -0
  112. package/src/handlers/entity/tolerance.ts +75 -0
  113. package/src/handlers/objects.ts +323 -139
  114. package/src/toSVG.ts +98 -7
  115. package/src/types/dimension-entity.ts +11 -0
  116. package/src/types/entity.ts +10 -0
  117. package/src/types/image-entity.ts +35 -0
  118. package/src/types/index.ts +4 -0
  119. package/src/types/leader-entity.ts +40 -0
  120. package/src/types/options.ts +41 -0
  121. package/src/types/tables.ts +84 -0
  122. package/src/types/tolerance-entity.ts +20 -0
  123. package/src/types/underlay-entity.ts +35 -0
  124. package/src/util/escapeXmlText.ts +10 -0
  125. package/tools/browser_test_server.cjs +87 -0
  126. package/tools/ezdxf_generate_dimensions_all_types.py +246 -0
  127. package/tools/ezdxf_generate_dimensions_angular_3p.py +59 -0
  128. package/tools/ezdxf_generate_dimensions_large_scale.py +87 -0
  129. package/tools/ezdxf_regenerate_problem_fixtures.py +184 -0
  130. package/tools/ezdxf_validate_fixtures.py +165 -0
  131. package/docs/DIMENSION_SUMMARY.pt-BR.md +0 -248
  132. package/docs/IMPLEMENTED-2D-ENTITIES.pt-BR.md +0 -54
  133. package/docs/TEXT-MTEXT-DIMENSION-SUPPORT.pt-BR.md +0 -169
package/src/toSVG.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Box2 } from 'vecks'
2
2
 
3
3
  import denormalise from './denormalise'
4
- import dimensionToSVG from './dimensionToSVG'
4
+ import dimensionToSVG, { getDimensionGeometryBBox } from './dimensionToSVG'
5
5
  import entityToPolyline from './entityToPolyline'
6
6
  import getRGBForEntity from './getRGBForEntity'
7
+ import escapeXmlText from './util/escapeXmlText'
7
8
  import logger from './util/logger'
8
9
  import rgbToColorAttribute from './util/rgbToColorAttribute'
9
10
  import rotate from './util/rotate'
@@ -16,10 +17,13 @@ import type {
16
17
  DimensionEntity,
17
18
  EllipseEntity,
18
19
  Entity,
20
+ LeaderEntity,
19
21
  MTextEntity,
20
22
  ParsedDXF,
21
23
  SplineEntity,
22
24
  TextEntity,
25
+ ToleranceEntity,
26
+ ToSVGOptions,
23
27
  } from './types'
24
28
  import type { BoundsAndElement } from './types/svg'
25
29
 
@@ -90,6 +94,26 @@ const lwpolyline = (entity: Entity): BoundsAndElement => {
90
94
  )
91
95
  }
92
96
 
97
+ const leader = (entity: LeaderEntity): BoundsAndElement | null => {
98
+ if (!entity.vertices || entity.vertices.length < 2) return null
99
+
100
+ const bbox0 = entity.vertices.reduce(
101
+ (acc, p) => acc.expandByPoint({ x: p.x, y: p.y }),
102
+ new Box2(),
103
+ )
104
+ const d = entity.vertices.reduce((acc, p, i) => {
105
+ acc += i === 0 ? 'M' : 'L'
106
+ acc += p.x + ',' + p.y
107
+ return acc
108
+ }, '')
109
+
110
+ return transformBoundingBoxAndElement(
111
+ bbox0,
112
+ `<path d="${d}" />`,
113
+ entity.transforms ?? [],
114
+ )
115
+ }
116
+
93
117
 
94
118
  /**
95
119
  * Create a <circle /> element for the CIRCLE entity.
@@ -311,7 +335,7 @@ const text = (entity: TextEntity): BoundsAndElement => {
311
335
  .expandByPoint({ x: x + textWidth, y: y + height })
312
336
 
313
337
  const rotationDegrees = (rotation * 180) / Math.PI
314
- const element0 = `<text x="${x}" y="${y}" font-size="${height}" transform="rotate(${-rotationDegrees} ${x} ${y}) scale(1,-1) translate(0 ${-2 * y})">${content}</text>`
338
+ const element0 = `<text x="${x}" y="${y}" font-size="${height}" transform="rotate(${-rotationDegrees} ${x} ${y}) scale(1,-1) translate(0 ${-2 * y})">${escapeXmlText(content)}</text>`
315
339
 
316
340
  const { bbox, element } = addFlipXIfApplicable(entity, {
317
341
  bbox: bbox0,
@@ -340,8 +364,36 @@ const mtext = (entity: MTextEntity): BoundsAndElement => {
340
364
  ? Math.atan2(entity.xAxisY, entity.xAxisX)
341
365
  : 0
342
366
  const rotationDegrees = (rotation * 180) / Math.PI
367
+ const element0 = `<text x="${x}" y="${y}" font-size="${height}" transform="rotate(${-rotationDegrees} ${x} ${y}) scale(1,-1) translate(0 ${-2 * y})">${escapeXmlText(content)}</text>`
368
+
369
+ const { bbox, element } = addFlipXIfApplicable(entity, {
370
+ bbox: bbox0,
371
+ element: element0,
372
+ })
373
+ return transformBoundingBoxAndElement(bbox, element, entity.transforms ?? [])
374
+ }
375
+
376
+ /**
377
+ * Minimal <text /> fallback for TOLERANCE entities.
378
+ * DXF uses special control codes; we preserve the raw string.
379
+ */
380
+ const tolerance = (entity: ToleranceEntity): BoundsAndElement => {
381
+ const x = entity.insertionPoint?.x ?? 0
382
+ const y = entity.insertionPoint?.y ?? 0
383
+ const height = 1
384
+ const content = entity.text ?? ''
385
+
386
+ const rotation = entity.xAxisDirection
387
+ ? Math.atan2(entity.xAxisDirection.y, entity.xAxisDirection.x)
388
+ : 0
389
+ const rotationDegrees = (rotation * 180) / Math.PI
390
+
391
+ const textWidth = content.length * height * 0.6
392
+ const bbox0 = new Box2()
393
+ .expandByPoint({ x, y })
394
+ .expandByPoint({ x: x + textWidth, y: y + height })
343
395
 
344
- const element0 = `<text x="${x}" y="${y}" font-size="${height}" transform="rotate(${-rotationDegrees} ${x} ${y}) scale(1,-1) translate(0 ${-2 * y})">${content}</text>`
396
+ const element0 = `<text x="${x}" y="${y}" font-size="${height}" transform="rotate(${-rotationDegrees} ${x} ${y}) scale(1,-1) translate(0 ${-2 * y})">${escapeXmlText(content)}</text>`
345
397
 
346
398
  const { bbox, element } = addFlipXIfApplicable(entity, {
347
399
  bbox: bbox0,
@@ -356,8 +408,10 @@ const mtext = (entity: MTextEntity): BoundsAndElement => {
356
408
  const dimension = (
357
409
  entity: DimensionEntity,
358
410
  dimStyle?: any,
411
+ options?: ToSVGOptions,
412
+ viewport?: { width: number; height: number },
359
413
  ): BoundsAndElement => {
360
- const result = dimensionToSVG(entity, dimStyle)
414
+ const result = dimensionToSVG(entity, dimStyle, options, viewport)
361
415
  return transformBoundingBoxAndElement(
362
416
  result.bbox,
363
417
  result.element,
@@ -410,6 +464,8 @@ const bezier = (entity: SplineEntity): BoundsAndElement => {
410
464
  const entityToBoundsAndElement = (
411
465
  entity: Entity,
412
466
  dimStyles?: { [name: string]: any },
467
+ options?: ToSVGOptions,
468
+ viewport?: { width: number; height: number },
413
469
  ): BoundsAndElement | null => {
414
470
  switch (entity.type) {
415
471
  case 'CIRCLE':
@@ -430,7 +486,7 @@ const entityToBoundsAndElement = (
430
486
  const dimStyle = styleName && dimStyles
431
487
  ? dimStyles[styleName]
432
488
  : undefined
433
- return dimension(dimEntity, dimStyle)
489
+ return dimension(dimEntity, dimStyle, options, viewport)
434
490
  }
435
491
  case 'SPLINE': {
436
492
  const splineEntity = entity as SplineEntity
@@ -454,22 +510,57 @@ const entityToBoundsAndElement = (
454
510
  case 'LWPOLYLINE': {
455
511
  return lwpolyline(entity)
456
512
  }
513
+ case 'LEADER': {
514
+ return leader(entity as LeaderEntity)
515
+ }
516
+ case 'TOLERANCE': {
517
+ return tolerance(entity as ToleranceEntity)
518
+ }
457
519
  default:
458
520
  logger.warn('entity type not supported in SVG rendering:', entity.type)
459
521
  return null
460
522
  }
461
523
  }
462
524
 
463
- export default function toSVG(parsed: ParsedDXF): string {
525
+ export default function toSVG(parsed: ParsedDXF, options: ToSVGOptions = {}): string {
464
526
  const entities = denormalise(parsed)
465
527
  const dimStyles = parsed.tables.dimStyles
528
+
529
+ const geometryBBox = entities.reduce((acc: Box2, entity: Entity): Box2 => {
530
+ if (entity.type === 'DIMENSION') {
531
+ const bbox = getDimensionGeometryBBox(entity as DimensionEntity)
532
+ if (bbox.valid) {
533
+ acc.expandByPoint(bbox.min)
534
+ acc.expandByPoint(bbox.max)
535
+ }
536
+ return acc
537
+ }
538
+
539
+ const boundsAndElement = entityToBoundsAndElement(entity, dimStyles, options)
540
+ if (boundsAndElement?.bbox.valid) {
541
+ acc.expandByPoint(boundsAndElement.bbox.min)
542
+ acc.expandByPoint(boundsAndElement.bbox.max)
543
+ }
544
+ return acc
545
+ }, new Box2())
546
+
547
+ const viewport = geometryBBox.valid
548
+ ? {
549
+ width: geometryBBox.max.x - geometryBBox.min.x,
550
+ height: geometryBBox.max.y - geometryBBox.min.y,
551
+ }
552
+ : {
553
+ width: 0,
554
+ height: 0,
555
+ }
556
+
466
557
  const { bbox, elements } = entities.reduce(
467
558
  (
468
559
  acc: { bbox: Box2; elements: string[] },
469
560
  entity: Entity,
470
561
  ): { bbox: Box2; elements: string[] } => {
471
562
  const rgb = getRGBForEntity(parsed.tables.layers, entity)
472
- const boundsAndElement = entityToBoundsAndElement(entity, dimStyles)
563
+ const boundsAndElement = entityToBoundsAndElement(entity, dimStyles, options, viewport)
473
564
  // Ignore entities that don't produce SVG elements or have unsupported types
474
565
  if (boundsAndElement) {
475
566
  const { bbox, element } = boundsAndElement
@@ -7,6 +7,17 @@ export interface DimensionEntity extends BaseEntity {
7
7
  type: 'DIMENSION'
8
8
  block?: string
9
9
  start: Point3D
10
+ /**
11
+ * Angular vertex for DIMENSION type 5 (Angular 3-point).
12
+ * This is parsed from group codes 15/25/35.
13
+ */
14
+ angleVertex?: Point3D
15
+
16
+ /**
17
+ * Angular arc location point for DIMENSION angular types.
18
+ * This is parsed from group codes 16/26/36 (OCS in the DXF reference).
19
+ */
20
+ arcPoint?: Point3D
10
21
  measureStart: Point3D
11
22
  measureEnd: Point3D
12
23
  textMidpoint: Point3D
@@ -6,7 +6,9 @@ import type { CircleEntity } from './circle-entity'
6
6
  import type { DimensionEntity } from './dimension-entity'
7
7
  import type { EllipseEntity } from './ellipse-entity'
8
8
  import type { HatchEntity } from './hatch-entity'
9
+ import type { ImageEntity } from './image-entity'
9
10
  import type { InsertEntity } from './insert-entity'
11
+ import type { LeaderEntity } from './leader-entity'
10
12
  import type { LineEntity } from './line-entity'
11
13
  import type { MTextEntity } from './mtext-entity'
12
14
  import type { PointEntity } from './point-entity'
@@ -14,6 +16,8 @@ import type { PolylineEntity } from './polyline-entity'
14
16
  import type { SolidEntity } from './solid-entity'
15
17
  import type { SplineEntity } from './spline-entity'
16
18
  import type { TextEntity } from './text-entity'
19
+ import type { ToleranceEntity } from './tolerance-entity'
20
+ import type { DgnUnderlayEntity, DwfUnderlayEntity, PdfUnderlayEntity } from './underlay-entity'
17
21
 
18
22
  export type Entity =
19
23
  | LineEntity
@@ -28,5 +32,11 @@ export type Entity =
28
32
  | DimensionEntity
29
33
  | SolidEntity
30
34
  | InsertEntity
35
+ | ImageEntity
36
+ | LeaderEntity
37
+ | ToleranceEntity
38
+ | DwfUnderlayEntity
39
+ | DgnUnderlayEntity
40
+ | PdfUnderlayEntity
31
41
  | HatchEntity
32
42
  | BaseEntity
@@ -0,0 +1,35 @@
1
+ // IMAGE entity type
2
+
3
+ import type { BaseEntity } from './base-entity'
4
+ import type { Point3D } from './common'
5
+
6
+ export interface ImageEntity extends BaseEntity {
7
+ type: 'IMAGE'
8
+
9
+ insertionPoint: Point3D
10
+ uVector: Point3D
11
+ vVector: Point3D
12
+
13
+ /** Image size in pixels. */
14
+ pixelSizeX: number
15
+ pixelSizeY: number
16
+
17
+ /** Hard reference to IMAGEDEF object. */
18
+ imageDefHandle?: string
19
+
20
+ /** Hard reference to IMAGEDEF_REACTOR object. */
21
+ imageDefReactorHandle?: string
22
+
23
+ /** Image display properties bitmask. */
24
+ displayProperties?: number
25
+
26
+ /** Clipping state: 0 = Off; 1 = On. */
27
+ clippingState?: number
28
+
29
+ brightness?: number
30
+ contrast?: number
31
+ fade?: number
32
+
33
+ /** DXF class version. */
34
+ classVersion?: number
35
+ }
@@ -12,7 +12,9 @@ export * from './circle-entity'
12
12
  export * from './dimension-entity'
13
13
  export * from './ellipse-entity'
14
14
  export * from './hatch-entity'
15
+ export * from './image-entity'
15
16
  export * from './insert-entity'
17
+ export * from './leader-entity'
16
18
  export * from './line-entity'
17
19
  export * from './mtext-entity'
18
20
  export * from './ole2frame-entity'
@@ -21,6 +23,8 @@ export * from './polyline-entity'
21
23
  export * from './solid-entity'
22
24
  export * from './spline-entity'
23
25
  export * from './text-entity'
26
+ export * from './tolerance-entity'
27
+ export * from './underlay-entity'
24
28
  export * from './viewport-entity'
25
29
 
26
30
  // Union type
@@ -0,0 +1,40 @@
1
+ // LEADER entity type
2
+
3
+ import type { BaseEntity } from './base-entity'
4
+ import type { Point3D } from './common'
5
+
6
+ export interface LeaderEntity extends BaseEntity {
7
+ type: 'LEADER'
8
+
9
+ /** Leader vertices, in drawing coordinates. */
10
+ vertices: Point3D[]
11
+
12
+ /** Dimension style name. */
13
+ dimensionStyleName?: string
14
+
15
+ /** Arrowhead flag: 0 = Disabled; 1 = Enabled. */
16
+ arrowheadFlag?: number
17
+
18
+ /** Leader path type: 0 = Straight line segments; 1 = Spline. */
19
+ pathType?: number
20
+
21
+ /** Leader creation flag: 0 = text; 1 = tolerance; 2 = insert; 3 = none. */
22
+ creationFlag?: number
23
+
24
+ hooklineDirectionFlag?: number
25
+ hooklineFlag?: number
26
+
27
+ textHeight?: number
28
+ textWidth?: number
29
+
30
+ /** Color to use if leader's DIMCLR D = BYBLOCK. */
31
+ color?: number
32
+
33
+ /** Hard reference to associated annotation entity. */
34
+ annotationHandle?: string
35
+
36
+ normal?: Point3D
37
+ horizontalDirection?: Point3D
38
+ blockOffset?: Point3D
39
+ annotationOffset?: Point3D
40
+ }
@@ -4,9 +4,50 @@ export interface ToPolylinesOptions {
4
4
  interpolationsPerSplineSegment?: number
5
5
  }
6
6
 
7
+ /**
8
+ * Viewport percentage controls for DIMENSION autoScale.
9
+ *
10
+ * Each value is a percentage (0..100) of the viewport minimum dimension.
11
+ */
12
+ export interface DimensionAutoScaleViewportPercentages {
13
+ /** Arrowhead marker size (markerWidth/markerHeight) */
14
+ arrowSize?: number
15
+ /** Text height (`font-size`) */
16
+ textHeight?: number
17
+ /** Extension line offset from the measured points */
18
+ extLineOffset?: number
19
+ /** Extension line overshoot beyond the dimension line */
20
+ extLineExtension?: number
21
+ }
22
+
7
23
  export interface ToSVGOptions {
8
24
  width?: number
9
25
  height?: number
26
+ dimension?: {
27
+ /**
28
+ * Automatically scale DIMENSION arrow size, extension endpoints, and
29
+ * text height estimates based on the SVG viewport size.
30
+ */
31
+ autoScale?: boolean
32
+
33
+ /**
34
+ * Viewport reference size used by DIMENSION autoScale.
35
+ * Scale factor is: min(viewBoxWidth, viewBoxHeight) / autoScaleViewportReference.
36
+ * Default: 40.
37
+ */
38
+ autoScaleViewportReference?: number
39
+
40
+ /**
41
+ * Per-element viewport-percentage overrides for DIMENSION autoScale.
42
+ *
43
+ * When provided (and `autoScale` is enabled), these values set the final
44
+ * sizes directly as a percentage of the viewport minimum dimension:
45
+ * `size = min(viewBoxWidth, viewBoxHeight) * (percent / 100)`.
46
+ *
47
+ * Percent values are expected in the `0..100` range.
48
+ */
49
+ autoScaleViewportPercentages?: DimensionAutoScaleViewportPercentages
50
+ }
10
51
  }
11
52
 
12
53
  export interface Config {
@@ -1,6 +1,7 @@
1
1
  // DXF Tables parsing types
2
2
 
3
3
  import type { Point2D, Point3D } from './common'
4
+ import type { DXFTuple } from './dxf'
4
5
 
5
6
  /**
6
7
  * Partial point for parsing (all coordinates optional)
@@ -408,7 +409,90 @@ export interface DimStyleInternal {
408
409
  /**
409
410
  * DXF Objects section result
410
411
  */
412
+ export interface DictionaryObject {
413
+ type: 'DICTIONARY'
414
+ handle?: string | number
415
+ ownerHandle?: string | number
416
+ entries: Record<string, string>
417
+ }
418
+
419
+ export interface XRecordObject {
420
+ type: 'XRECORD'
421
+ handle?: string | number
422
+ ownerHandle?: string | number
423
+
424
+ /** Raw tuples for downstream consumers (excluding the initial 0/XRECORD tuple). */
425
+ tuples: DXFTuple[]
426
+ }
427
+
428
+ export interface ImageDefObject {
429
+ type: 'IMAGEDEF'
430
+ handle?: string | number
431
+
432
+ /** Soft-pointer ID/handle to the ACAD_IMAGE_dict dictionary (when present). */
433
+ ownerHandle?: string | number
434
+
435
+ /** File name of the referenced image. */
436
+ fileName?: string
437
+
438
+ /** Image size in pixels (when available). */
439
+ pixelSizeX?: number
440
+ pixelSizeY?: number
441
+
442
+ /** Raw tuples for downstream consumers (excluding the initial 0/IMAGEDEF tuple). */
443
+ tuples: DXFTuple[]
444
+ }
445
+
446
+ export interface ImageDefReactorObject {
447
+ type: 'IMAGEDEF_REACTOR'
448
+ handle?: string | number
449
+
450
+ /** Object ID/handle for the associated IMAGE entity (when present). */
451
+ imageHandle?: string | number
452
+
453
+ /** Raw tuples for downstream consumers (excluding the initial 0/IMAGEDEF_REACTOR tuple). */
454
+ tuples: DXFTuple[]
455
+ }
456
+
457
+ export type UnderlayDefinitionObjectType =
458
+ | 'UNDERLAYDEFINITION'
459
+ | 'PDFDEFINITION'
460
+ | 'DWFDEFINITION'
461
+ | 'DGNDEFINITION'
462
+
463
+ export interface UnderlayDefinitionObject {
464
+ type: UnderlayDefinitionObjectType
465
+ handle?: string | number
466
+
467
+ /** Soft-pointer ID/handle to the owning dictionary (when present). */
468
+ ownerHandle?: string | number
469
+
470
+ /** File name or path of the referenced underlay. */
471
+ fileName?: string
472
+
473
+ /** Underlay name within the file (e.g., sheet name). */
474
+ underlayName?: string
475
+
476
+ /** Raw tuples for downstream consumers (excluding the initial 0/<TYPE> tuple). */
477
+ tuples: DXFTuple[]
478
+ }
479
+
411
480
  export interface ParsedObjects {
412
481
  /** Layout objects */
413
482
  layouts: LayoutInternal[]
483
+
484
+ /** DICTIONARY objects keyed by handle */
485
+ dictionaries?: Record<string, DictionaryObject>
486
+
487
+ /** XRECORD objects keyed by handle */
488
+ xRecords?: Record<string, XRecordObject>
489
+
490
+ /** IMAGEDEF objects keyed by handle */
491
+ imageDefs?: Record<string, ImageDefObject>
492
+
493
+ /** IMAGEDEF_REACTOR objects keyed by handle */
494
+ imageDefReactors?: Record<string, ImageDefReactorObject>
495
+
496
+ /** UNDERLAYDEFINITION objects keyed by handle */
497
+ underlayDefinitions?: Record<string, UnderlayDefinitionObject>
414
498
  }
@@ -0,0 +1,20 @@
1
+ // TOLERANCE entity type (feature control frame)
2
+
3
+ import type { BaseEntity } from './base-entity'
4
+ import type { Point3D } from './common'
5
+
6
+ export interface ToleranceEntity extends BaseEntity {
7
+ type: 'TOLERANCE'
8
+
9
+ /** Insertion point (WCS). */
10
+ insertionPoint: Point3D
11
+
12
+ /** Raw tolerance string (may include control codes like %%v). */
13
+ text?: string
14
+
15
+ /** Dimension style name (group code 3). */
16
+ dimensionStyleName?: string
17
+
18
+ /** X-axis direction vector (group codes 11/21/31). */
19
+ xAxisDirection?: Point3D
20
+ }
@@ -0,0 +1,35 @@
1
+ // UNDERLAY entity types (DWFUNDERLAY / DGNUNDERLAY / PDFUNDERLAY)
2
+
3
+ import type { BaseEntity } from './base-entity'
4
+ import type { Point3D } from './common'
5
+
6
+ export interface UnderlayReferenceEntityBase extends BaseEntity {
7
+ insertionPoint: Point3D
8
+ scale: Point3D
9
+ rotation?: number
10
+ normal?: Point3D
11
+
12
+ /** Hard reference to UNDERLAYDEFINITION object. */
13
+ underlayDefinitionHandle?: string
14
+
15
+ /** Display properties bitmask. */
16
+ flags?: number
17
+
18
+ /** Contrast in range [0, 100]. */
19
+ contrast?: number
20
+
21
+ /** Fade in range [0, 100]. */
22
+ fade?: number
23
+ }
24
+
25
+ export interface DwfUnderlayEntity extends UnderlayReferenceEntityBase {
26
+ type: 'DWFUNDERLAY'
27
+ }
28
+
29
+ export interface DgnUnderlayEntity extends UnderlayReferenceEntityBase {
30
+ type: 'DGNUNDERLAY'
31
+ }
32
+
33
+ export interface PdfUnderlayEntity extends UnderlayReferenceEntityBase {
34
+ type: 'PDFUNDERLAY'
35
+ }
@@ -0,0 +1,10 @@
1
+ export default function escapeXmlText(value: string): string {
2
+ // Escape text for use inside XML/SVG text nodes.
3
+ // Keep it small and dependency-free.
4
+ return value
5
+ .split('&').join('&amp;')
6
+ .split('<').join('&lt;')
7
+ .split('>').join('&gt;')
8
+ .split('"').join('&quot;')
9
+ .split("'").join('&apos;')
10
+ }
@@ -0,0 +1,87 @@
1
+ const http = require('node:http')
2
+ const { createReadStream, existsSync, statSync } = require('node:fs')
3
+ const { extname, join, normalize, resolve } = require('node:path')
4
+
5
+ const projectRoot = resolve(__dirname, '..')
6
+ const port = Number(process.env.PORT || 4173)
7
+
8
+ const harnessHtmlPath = join(projectRoot, 'test', 'browser', 'harness.html')
9
+ const distBundlePath = join(projectRoot, 'dist', 'dxf.js')
10
+ const fixturesDir = join(projectRoot, 'test', 'resources')
11
+
12
+ function sendText(res, statusCode, text, contentType = 'text/plain; charset=utf-8') {
13
+ res.writeHead(statusCode, { 'Content-Type': contentType })
14
+ res.end(text)
15
+ }
16
+
17
+ function contentTypeForPath(filePath) {
18
+ const ext = extname(filePath).toLowerCase()
19
+ switch (ext) {
20
+ case '.html':
21
+ return 'text/html; charset=utf-8'
22
+ case '.js':
23
+ return 'text/javascript; charset=utf-8'
24
+ case '.dxf':
25
+ return 'text/plain; charset=utf-8'
26
+ case '.json':
27
+ return 'application/json; charset=utf-8'
28
+ default:
29
+ return 'application/octet-stream'
30
+ }
31
+ }
32
+
33
+ function safeJoin(baseDir, requestedPath) {
34
+ const cleaned = requestedPath.replace(/^\/+/, '')
35
+ const target = normalize(join(baseDir, cleaned))
36
+ const resolvedBase = resolve(baseDir)
37
+ const resolvedTarget = resolve(target)
38
+ if (!resolvedTarget.startsWith(resolvedBase + '/')) {
39
+ return null
40
+ }
41
+ return resolvedTarget
42
+ }
43
+
44
+ const server = http.createServer((req, res) => {
45
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
46
+ const pathname = url.pathname
47
+
48
+ if (pathname === '/' || pathname === '/harness') {
49
+ if (!existsSync(harnessHtmlPath)) {
50
+ return sendText(res, 500, 'Missing test/browser/harness.html')
51
+ }
52
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
53
+ return createReadStream(harnessHtmlPath).pipe(res)
54
+ }
55
+
56
+ if (pathname === '/dist/dxf.js') {
57
+ if (!existsSync(distBundlePath)) {
58
+ return sendText(
59
+ res,
60
+ 500,
61
+ 'Missing dist/dxf.js. Run `yarn dist` before running browser tests.'
62
+ )
63
+ }
64
+ res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8' })
65
+ return createReadStream(distBundlePath).pipe(res)
66
+ }
67
+
68
+ if (pathname.startsWith('/fixtures/')) {
69
+ const fixtureName = pathname.slice('/fixtures/'.length)
70
+ const fixturePath = safeJoin(fixturesDir, fixtureName)
71
+ if (!fixturePath) {
72
+ return sendText(res, 400, 'Invalid fixture path')
73
+ }
74
+ if (!existsSync(fixturePath) || !statSync(fixturePath).isFile()) {
75
+ return sendText(res, 404, 'Fixture not found')
76
+ }
77
+ res.writeHead(200, { 'Content-Type': contentTypeForPath(fixturePath) })
78
+ return createReadStream(fixturePath).pipe(res)
79
+ }
80
+
81
+ return sendText(res, 404, 'Not found')
82
+ })
83
+
84
+ server.listen(port, () => {
85
+ // Used by Playwright webServer readiness checks.
86
+ console.log(`Browser test server listening on http://localhost:${port}`)
87
+ })