pycadkit 0.2.1__tar.gz → 0.3.1__tar.gz

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 (180) hide show
  1. {pycadkit-0.2.1 → pycadkit-0.3.1}/Cargo.lock +10 -10
  2. {pycadkit-0.2.1 → pycadkit-0.3.1}/Cargo.toml +7 -7
  3. {pycadkit-0.2.1 → pycadkit-0.3.1}/PKG-INFO +1 -1
  4. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/examples/interchange.rs +1 -0
  5. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/model.rs +30 -2
  6. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/lib.rs +1 -1
  7. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/map/mod.rs +202 -64
  8. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/map/v8.rs +0 -42
  9. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/decode_v8.rs +9 -1
  10. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/element.rs +6 -0
  11. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/v7.rs +3 -0
  12. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/encode.rs +98 -23
  13. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/mod.rs +308 -35
  14. pycadkit-0.3.1/crates/cadkit-dgn/src/writer/raster.rs +323 -0
  15. pycadkit-0.3.1/crates/cadkit-dgn/src/writer/raster_template.rs +571 -0
  16. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tables.rs +14 -1
  17. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tests/application.rs +1 -0
  18. pycadkit-0.3.1/crates/cadkit-dgn/src/writer/tests/microstation.rs +531 -0
  19. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tests/raster.rs +4 -3
  20. pycadkit-0.3.1/crates/cadkit-dgn/src/writer/tests/raster_template.rs +649 -0
  21. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tests/regressions.rs +3 -0
  22. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tests.rs +6 -0
  23. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/text.rs +12 -16
  24. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/tests/common/mod.rs +25 -0
  25. pycadkit-0.3.1/crates/cadkit-dgn/tests/private_corpus.rs +345 -0
  26. pycadkit-0.3.1/crates/cadkit-dgn/tests/v8_range.rs +61 -0
  27. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/mapping/entity.rs +1 -0
  28. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/entities.rs +1 -0
  29. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/review_fixes.rs +1 -0
  30. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/synthetic_write.rs +2 -0
  31. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/mapping.rs +1 -0
  32. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/tests/support/export_cases.rs +1 -0
  33. pycadkit-0.2.1/crates/cadkit-dgn/src/writer/raster.rs +0 -245
  34. pycadkit-0.2.1/crates/cadkit-dgn/tests/private_corpus.rs +0 -151
  35. {pycadkit-0.2.1 → pycadkit-0.3.1}/LICENSE-APACHE +0 -0
  36. {pycadkit-0.2.1 → pycadkit-0.3.1}/LICENSE-MIT +0 -0
  37. {pycadkit-0.2.1 → pycadkit-0.3.1}/NOTICE +0 -0
  38. {pycadkit-0.2.1 → pycadkit-0.3.1}/README.md +0 -0
  39. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/Cargo.toml +0 -0
  40. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/LICENSE-APACHE +0 -0
  41. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/LICENSE-MIT +0 -0
  42. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/NOTICE +0 -0
  43. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/README.md +0 -0
  44. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit/src/lib.rs +0 -0
  45. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/Cargo.toml +0 -0
  46. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/LICENSE-APACHE +0 -0
  47. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/LICENSE-MIT +0 -0
  48. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/NOTICE +0 -0
  49. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/README.md +0 -0
  50. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/aci.rs +0 -0
  51. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/bytes.rs +0 -0
  52. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/error.rs +0 -0
  53. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/export.rs +0 -0
  54. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/geom.rs +0 -0
  55. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/geom_ops.rs +0 -0
  56. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/lib.rs +0 -0
  57. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/ops.rs +0 -0
  58. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/options.rs +0 -0
  59. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/polygon.rs +0 -0
  60. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/svg.rs +0 -0
  61. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/text.rs +0 -0
  62. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-core/src/units.rs +0 -0
  63. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/Cargo.toml +0 -0
  64. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/LICENSE-APACHE +0 -0
  65. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/LICENSE-MIT +0 -0
  66. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/NOTICE +0 -0
  67. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/README.md +0 -0
  68. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/examples/dgn_summary.rs +0 -0
  69. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/examples/raster_structure.rs +0 -0
  70. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/budget.rs +0 -0
  71. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/cfb.rs +0 -0
  72. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/fuzz_tests.rs +0 -0
  73. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/le.rs +0 -0
  74. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/level_names.rs +0 -0
  75. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/map/v7.rs +0 -0
  76. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/linkage.rs +0 -0
  77. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/mod.rs +0 -0
  78. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/native/v8.rs +0 -0
  79. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/palette.rs +0 -0
  80. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/text.rs +0 -0
  81. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/container.rs +0 -0
  82. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/polyline.rs +0 -0
  83. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/writer/tests/text.rs +0 -0
  84. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/src/zlib.rs +0 -0
  85. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/tests/robustness.rs +0 -0
  86. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/tests/v7_corpus.rs +0 -0
  87. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/tests/v8_oracle.rs +0 -0
  88. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dgn/tests/v8_public.rs +0 -0
  89. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/Cargo.toml +0 -0
  90. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/LICENSE-APACHE +0 -0
  91. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/LICENSE-MIT +0 -0
  92. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/NOTICE +0 -0
  93. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/README.md +0 -0
  94. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/examples/docsummary.rs +0 -0
  95. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/examples/dwgdump.rs +0 -0
  96. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/bits.rs +0 -0
  97. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/classes.rs +0 -0
  98. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/codepage.rs +0 -0
  99. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/lz77_r2004.rs +0 -0
  100. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/lz77_r2007.rs +0 -0
  101. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/mod.rs +0 -0
  102. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/r13.rs +0 -0
  103. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/r2004.rs +0 -0
  104. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/r2007.rs +0 -0
  105. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/container/reed_solomon.rs +0 -0
  106. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/crc.rs +0 -0
  107. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/handles.rs +0 -0
  108. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/header.rs +0 -0
  109. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/lib.rs +0 -0
  110. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/mapping/mod.rs +0 -0
  111. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/native/entities.rs +0 -0
  112. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/native/mod.rs +0 -0
  113. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/annotation.rs +0 -0
  114. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/curves.rs +0 -0
  115. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/entities.rs +0 -0
  116. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/hatch.rs +0 -0
  117. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/misc.rs +0 -0
  118. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/mod.rs +0 -0
  119. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/registry.rs +0 -0
  120. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/object/tables.rs +0 -0
  121. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/reader.rs +0 -0
  122. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/src/version.rs +0 -0
  123. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/tests/corpus.rs +0 -0
  124. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/tests/oracle.rs +0 -0
  125. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dwg/tests/robustness.rs +0 -0
  126. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/Cargo.toml +0 -0
  127. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/LICENSE-APACHE +0 -0
  128. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/LICENSE-MIT +0 -0
  129. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/NOTICE +0 -0
  130. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/README.md +0 -0
  131. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/examples/dump.rs +0 -0
  132. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/lib.rs +0 -0
  133. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/native/mod.rs +0 -0
  134. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/native/text.rs +0 -0
  135. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/ocs.rs +0 -0
  136. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/common.rs +0 -0
  137. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/hatch.rs +0 -0
  138. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/mod.rs +0 -0
  139. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/records.rs +0 -0
  140. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/reader/tables.rs +0 -0
  141. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/writer/curves.rs +0 -0
  142. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/writer/entities.rs +0 -0
  143. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/writer/mod.rs +0 -0
  144. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/src/writer/out.rs +0 -0
  145. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/ascii_vs_binary.rs +0 -0
  146. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/common/mod.rs +0 -0
  147. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/compare_reader_with_ezdxf.py +0 -0
  148. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/robustness.rs +0 -0
  149. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/roundtrip.rs +0 -0
  150. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/synthetic_read.rs +0 -0
  151. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-dxf/tests/validate_with_ezdxf.py +0 -0
  152. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/Cargo.toml +0 -0
  153. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/LICENSE-APACHE +0 -0
  154. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/LICENSE-MIT +0 -0
  155. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/NOTICE +0 -0
  156. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/README.md +0 -0
  157. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/curves.rs +0 -0
  158. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/export.rs +0 -0
  159. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/geometry.rs +0 -0
  160. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/lib.rs +0 -0
  161. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/native.rs +0 -0
  162. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/tkgm.rs +0 -0
  163. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/validate.rs +0 -0
  164. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/src/xml.rs +0 -0
  165. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/tests/roundtrip.rs +0 -0
  166. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-gml/tests/support/tkgm_cases.rs +0 -0
  167. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/Cargo.toml +0 -0
  168. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/LICENSE-APACHE +0 -0
  169. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/LICENSE-MIT +0 -0
  170. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/NOTICE +0 -0
  171. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/README.md +0 -0
  172. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/src/lib.rs +0 -0
  173. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/tests/conftest.py +0 -0
  174. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/tests/test_cadkit.py +0 -0
  175. {pycadkit-0.2.1 → pycadkit-0.3.1}/crates/cadkit-py/tests/test_citygml.py +0 -0
  176. {pycadkit-0.2.1 → pycadkit-0.3.1}/pyproject.toml +0 -0
  177. {pycadkit-0.2.1 → pycadkit-0.3.1}/python/cadkit/__init__.py +0 -0
  178. {pycadkit-0.2.1 → pycadkit-0.3.1}/python/cadkit/__init__.pyi +0 -0
  179. {pycadkit-0.2.1 → pycadkit-0.3.1}/python/cadkit/_cadkit.pyi +0 -0
  180. {pycadkit-0.2.1 → pycadkit-0.3.1}/python/cadkit/py.typed +0 -0
@@ -66,7 +66,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
66
66
 
67
67
  [[package]]
68
68
  name = "cadkit"
69
- version = "0.2.1"
69
+ version = "0.3.1"
70
70
  dependencies = [
71
71
  "cadkit-core",
72
72
  "cadkit-dgn",
@@ -78,7 +78,7 @@ dependencies = [
78
78
 
79
79
  [[package]]
80
80
  name = "cadkit-capi"
81
- version = "0.2.1"
81
+ version = "0.3.1"
82
82
  dependencies = [
83
83
  "cadkit",
84
84
  "serde",
@@ -87,7 +87,7 @@ dependencies = [
87
87
 
88
88
  [[package]]
89
89
  name = "cadkit-cli"
90
- version = "0.2.1"
90
+ version = "0.3.1"
91
91
  dependencies = [
92
92
  "cadkit",
93
93
  "clap",
@@ -96,7 +96,7 @@ dependencies = [
96
96
 
97
97
  [[package]]
98
98
  name = "cadkit-core"
99
- version = "0.2.1"
99
+ version = "0.3.1"
100
100
  dependencies = [
101
101
  "serde",
102
102
  "serde_json",
@@ -105,7 +105,7 @@ dependencies = [
105
105
 
106
106
  [[package]]
107
107
  name = "cadkit-dgn"
108
- version = "0.2.1"
108
+ version = "0.3.1"
109
109
  dependencies = [
110
110
  "cadkit-core",
111
111
  "cfb",
@@ -116,7 +116,7 @@ dependencies = [
116
116
 
117
117
  [[package]]
118
118
  name = "cadkit-dwg"
119
- version = "0.2.1"
119
+ version = "0.3.1"
120
120
  dependencies = [
121
121
  "cadkit-core",
122
122
  "cadkit-dxf",
@@ -126,7 +126,7 @@ dependencies = [
126
126
 
127
127
  [[package]]
128
128
  name = "cadkit-dxf"
129
- version = "0.2.1"
129
+ version = "0.3.1"
130
130
  dependencies = [
131
131
  "cadkit-core",
132
132
  "encoding_rs",
@@ -135,7 +135,7 @@ dependencies = [
135
135
 
136
136
  [[package]]
137
137
  name = "cadkit-gml"
138
- version = "0.2.1"
138
+ version = "0.3.1"
139
139
  dependencies = [
140
140
  "cadkit-core",
141
141
  "quick-xml",
@@ -145,7 +145,7 @@ dependencies = [
145
145
 
146
146
  [[package]]
147
147
  name = "cadkit-py"
148
- version = "0.2.1"
148
+ version = "0.3.1"
149
149
  dependencies = [
150
150
  "cadkit",
151
151
  "pyo3",
@@ -155,7 +155,7 @@ dependencies = [
155
155
 
156
156
  [[package]]
157
157
  name = "cadkit-wasm"
158
- version = "0.2.1"
158
+ version = "0.3.1"
159
159
  dependencies = [
160
160
  "cadkit",
161
161
  "js-sys",
@@ -4,7 +4,7 @@ members = ["crates/*"]
4
4
  exclude = ["fuzz"]
5
5
 
6
6
  [workspace.package]
7
- version = "0.2.1"
7
+ version = "0.3.1"
8
8
  edition = "2024"
9
9
  rust-version = "1.85"
10
10
  license = "MIT OR Apache-2.0"
@@ -14,12 +14,12 @@ keywords = ["cad", "dwg", "dgn", "dxf", "citygml"]
14
14
  categories = ["parser-implementations", "graphics", "encoding", "science::geo"]
15
15
 
16
16
  [workspace.dependencies]
17
- cadkit-core = { path = "crates/cadkit-core", version = "0.2.1" }
18
- cadkit-dgn = { path = "crates/cadkit-dgn", version = "0.2.1" }
19
- cadkit-dwg = { path = "crates/cadkit-dwg", version = "0.2.1" }
20
- cadkit-dxf = { path = "crates/cadkit-dxf", version = "0.2.1" }
21
- cadkit-gml = { path = "crates/cadkit-gml", version = "0.2.1" }
22
- cadkit = { path = "crates/cadkit", version = "0.2.1" }
17
+ cadkit-core = { path = "crates/cadkit-core", version = "0.3.1" }
18
+ cadkit-dgn = { path = "crates/cadkit-dgn", version = "0.3.1" }
19
+ cadkit-dwg = { path = "crates/cadkit-dwg", version = "0.3.1" }
20
+ cadkit-dxf = { path = "crates/cadkit-dxf", version = "0.3.1" }
21
+ cadkit-gml = { path = "crates/cadkit-gml", version = "0.3.1" }
22
+ cadkit = { path = "crates/cadkit", version = "0.3.1" }
23
23
  serde = { version = "1", features = ["derive"] }
24
24
  serde_json = "1"
25
25
  thiserror = "2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycadkit
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -75,6 +75,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
75
75
  set: Some("Örnek".into()),
76
76
  position: None,
77
77
  invisible: true,
78
+ ..Default::default()
78
79
  });
79
80
  if let EntityKind::Polygon {
80
81
  exterior,
@@ -722,7 +722,7 @@ pub enum Lineweight {
722
722
  }
723
723
 
724
724
  /// Non-graphic data attached to an entity (DWG ATTRIB, DGN tag, item-type property).
725
- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
725
+ #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
726
726
  pub struct Attribute {
727
727
  /// Tag / property name.
728
728
  pub tag: String,
@@ -734,13 +734,41 @@ pub struct Attribute {
734
734
  pub position: Option<Point3>,
735
735
  /// The attribute is not displayed.
736
736
  pub invisible: bool,
737
+ /// Layer of the attribute's own record when the format stores one (a DGN tag
738
+ /// element has its own level). `None` uses the owning entity's layer.
739
+ #[serde(default, skip_serializing_if = "Option::is_none")]
740
+ pub layer: Option<String>,
741
+ /// How the value text is drawn at `position`; `None` when unknown or not drawn.
742
+ #[serde(default, skip_serializing_if = "Option::is_none")]
743
+ pub display: Option<AttributeDisplay>,
744
+ /// Format-specific data with no neutral field (e.g. `dgn.color_index`, `dgn.weight`).
745
+ #[serde(default, skip_serializing_if = "Props::is_empty")]
746
+ pub props: Props,
747
+ }
748
+
749
+ /// Text presentation of a displayed attribute value.
750
+ #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
751
+ pub struct AttributeDisplay {
752
+ /// Text height, in drawing units.
753
+ pub height: f64,
754
+ /// Character width, in drawing units (DGN stores it separately from the height).
755
+ pub width: f64,
756
+ /// Text style (DGN font) name; `None` leaves the choice to the writer's default.
757
+ pub style: Option<String>,
758
+ /// Horizontal alignment of the text about `Attribute::position`.
759
+ pub halign: HAlign,
760
+ /// Vertical alignment of the text about `Attribute::position`.
761
+ pub valign: VAlign,
762
+ /// Rotation about the drawing Z axis, in radians.
763
+ pub rotation: f64,
737
764
  }
738
765
 
739
766
  /// A loosely typed value for attributes and props.
740
- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
767
+ #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
741
768
  #[serde(untagged)]
742
769
  pub enum Value {
743
770
  /// No value.
771
+ #[default]
744
772
  Null,
745
773
  /// Boolean.
746
774
  Bool(bool),
@@ -10,7 +10,7 @@
10
10
  pub mod cfb;
11
11
  pub mod native;
12
12
  pub mod writer;
13
- pub use writer::{WriteOptions, repack_v8, write_v8};
13
+ pub use writer::{WriteOptions, repack_v8, seed_has_raster_template, write_v8};
14
14
  pub mod palette;
15
15
 
16
16
  mod budget;
@@ -8,8 +8,8 @@ use std::f64::consts::{FRAC_PI_2, TAU};
8
8
 
9
9
  use cadkit_core::geom_ops::{arbitrary_axes, clamped_uniform_knots};
10
10
  use cadkit_core::{
11
- Attribute, Color, Entity, EntityKind, GroupKind, HAlign, LengthUnit, Linetype, Lineweight,
12
- Point3, Props, Raw, TextStyle, VAlign, Value, Vec3, Vertex, Warning,
11
+ Attribute, AttributeDisplay, Color, Entity, EntityKind, GroupKind, HAlign, LengthUnit,
12
+ Linetype, Lineweight, Point3, Props, Raw, TextStyle, VAlign, Value, Vec3, Vertex, Warning,
13
13
  };
14
14
 
15
15
  use crate::native::element::{
@@ -203,8 +203,6 @@ pub(crate) struct Mapper {
203
203
  pub(crate) xattributes: HashMap<u64, Vec<u32>>,
204
204
  /// Raster frame id -> image path.
205
205
  pub(crate) raster_paths: HashMap<u64, (Option<String>, Option<String>)>,
206
- /// Raster frames whose stored extent corner and file are shared with another frame.
207
- pub(crate) shared_raster_extent: HashSet<u64>,
208
206
  /// Aggregated warnings: code -> (count, first message).
209
207
  counts: BTreeMap<String, (u64, String)>,
210
208
  pub(crate) warnings: Vec<Warning>,
@@ -226,7 +224,6 @@ impl Mapper {
226
224
  attached: HashSet::new(),
227
225
  xattributes: HashMap::new(),
228
226
  raster_paths: HashMap::new(),
229
- shared_raster_extent: HashSet::new(),
230
227
  counts: BTreeMap::new(),
231
228
  warnings: Vec::new(),
232
229
  }
@@ -402,6 +399,14 @@ impl Mapper {
402
399
  );
403
400
  }
404
401
  p.insert("dgn.properties".into(), Value::Int(i64::from(h.properties)));
402
+ // Undocumented type-word flags MicroStation sets on its graphics (`0x00C0`);
403
+ // kept so a rewrite reproduces them.
404
+ if h.type_flags & 0x00c0 != 0 {
405
+ p.insert(
406
+ "dgn.type_flags".into(),
407
+ Value::Int(i64::from(h.type_flags & 0x00c0)),
408
+ );
409
+ }
405
410
  if h.properties & 0x8000 != 0 && matches!(h.type_code, 6 | 14) {
406
411
  p.insert("dgn.hole".into(), Value::Bool(true));
407
412
  }
@@ -441,7 +446,10 @@ impl Mapper {
441
446
  }
442
447
  if let Some(id) = h.id {
443
448
  if let Some(attrs) = self.pending_tags.remove(&id) {
444
- e.attributes = attrs.into_iter().map(|(_, a)| a).collect();
449
+ e.attributes = attrs
450
+ .into_iter()
451
+ .map(|(_, a)| relative_to_owner(a, &e))
452
+ .collect();
445
453
  }
446
454
  }
447
455
  e
@@ -561,7 +569,8 @@ impl Mapper {
561
569
  type_name: "dgn.type_37".into(),
562
570
  },
563
571
  );
564
- e.attributes.push(self.attribute(t, &item.el.header, xf));
572
+ let a = self.attribute(t, &item.el.header, xf);
573
+ e.attributes.push(relative_to_owner(a, &e));
565
574
  return Some(e);
566
575
  }
567
576
  ElementData::RasterFrame { .. } => self.image(item, xf),
@@ -662,7 +671,7 @@ impl Mapper {
662
671
  e.props.insert("dgn.point_string".into(), Value::Bool(true));
663
672
  }
664
673
  ElementData::RasterFrame { matrix, .. } => {
665
- // Pixel-to-UOR matrix as stored; the image rectangle comes from the range.
674
+ // Pixel-to-UOR matrix as stored; the range only checks the placement.
666
675
  if let Some([lo, hi]) = item.el.header.range {
667
676
  let (a, b) = (xf.pt(lo), xf.pt(hi));
668
677
  e.props.insert(
@@ -670,19 +679,23 @@ impl Mapper {
670
679
  Value::List([a.x, a.y, a.z, b.x, b.y, b.z].map(Value::Float).to_vec()),
671
680
  );
672
681
  }
673
- if item
674
- .el
675
- .header
676
- .id
677
- .is_some_and(|id| self.shared_raster_extent.contains(&id))
678
- {
679
- e.props
680
- .insert("dgn.raster_extent_shared".into(), Value::Bool(true));
681
- }
682
682
  e.props.insert(
683
683
  "dgn.raster_matrix".into(),
684
684
  Value::List(matrix.iter().map(|v| Value::Float(*v)).collect()),
685
685
  );
686
+ // `0x118` / `0x120`: horizontal and vertical resolution in dots per inch.
687
+ let dpi = crate::le::f64_at(item.raw, 0x118);
688
+ if let Some(x) = dpi {
689
+ e.props.insert("dgn.raster_dpi".into(), Value::Float(x));
690
+ }
691
+ if let Some(y) = crate::le::f64_at(item.raw, 0x120).filter(|y| Some(*y) != dpi) {
692
+ e.props.insert("dgn.raster_dpi_y".into(), Value::Float(y));
693
+ }
694
+ if let Some((rotation, skew)) = frame_angles(matrix) {
695
+ e.props
696
+ .insert("dgn.raster_rotation".into(), Value::Float(rotation));
697
+ e.props.insert("dgn.raster_skew".into(), Value::Float(skew));
698
+ }
686
699
  let id = item.el.header.id.unwrap_or(0);
687
700
  if let Some((name, full)) = self.raster_paths.get(&id) {
688
701
  if let Some(n) = name {
@@ -873,34 +886,33 @@ impl Mapper {
873
886
  fn image(&mut self, item: &Item<'_>, xf: &Xf) -> EntityKind {
874
887
  let id = item.el.header.id.unwrap_or(0);
875
888
  let range = item.el.header.range;
876
- let (matrix_placement, axes) = match &item.el.data {
877
- ElementData::RasterFrame { matrix, corner } => {
878
- let [m00, m01, _, _, m10, m11, _, _, m20, m21, ..] = *matrix;
879
- (
880
- frame_placement(matrix, *corner),
881
- Some((Vec3::new(m00, m10, m20), Vec3::new(m01, m11, m21))),
882
- )
883
- }
884
- _ => (None, None),
889
+ let (matrix_placement, pixels, axes) = match &item.el.data {
890
+ ElementData::RasterFrame { matrix, corner } => (
891
+ frame_placement(matrix, *corner),
892
+ frame_pixels(matrix, *corner),
893
+ Some(frame_axes(matrix)),
894
+ ),
895
+ _ => (None, None, None),
885
896
  };
886
- // The element range is MicroStation's own bounding box: a matrix placement that does
887
- // not fit inside it is rejected in favour of a placement derived from the range.
888
- let placement = match (matrix_placement, range) {
897
+ // The element range is MicroStation's own bounding box. It only checks the matrix
898
+ // placement: one that does not fit inside it is replaced by a placement derived from
899
+ // the range, and the pixel size is then not trusted either.
900
+ let (placement, size_px) = match (matrix_placement, range) {
889
901
  (Some(p), Some(r)) if !within_range(&p, &r, RASTER_RANGE_TOLERANCE) => {
890
902
  self.note("dgn.raster_outside_range", || {
891
903
  "raster transform places the image outside its element range; the range was used"
892
904
  .into()
893
905
  });
894
- Some(range_placement(&r, axes))
906
+ (Some(range_placement(&r, axes)), None)
895
907
  }
896
- (Some(p), _) => Some(p),
908
+ (Some(p), _) => (Some(p), pixels),
897
909
  (None, Some(r)) => {
898
910
  self.note("dgn.raster_range_placement", || {
899
911
  "raster frame placed from its range (no usable transform)".into()
900
912
  });
901
- Some(range_placement(&r, axes))
913
+ (Some(range_placement(&r, axes)), None)
902
914
  }
903
- (None, None) => None,
915
+ (None, None) => (None, None),
904
916
  };
905
917
  let Some((origin, u, v)) = placement else {
906
918
  return EntityKind::Unknown {
@@ -921,13 +933,14 @@ impl Mapper {
921
933
  position: xf.pt(origin),
922
934
  u_vector: tidy(u.scaled(xf.scale)),
923
935
  v_vector: tidy(v.scaled(xf.scale)),
924
- size_px: None,
936
+ size_px,
925
937
  }
926
938
  }
927
939
 
928
- /// A tag as an attribute (`position` = origin + offset).
940
+ /// A tag as an attribute (`position` = origin + offset). The tag element's own level,
941
+ /// its text presentation and its symbology are kept so a writer can reproduce it.
929
942
  pub(crate) fn attribute(
930
- &self,
943
+ &mut self,
931
944
  t: &TagData,
932
945
  h: &crate::native::element::ElementHeader,
933
946
  xf: &Xf,
@@ -937,18 +950,66 @@ impl Mapper {
937
950
  .or(t.set_number.map(u64::from))
938
951
  .and_then(|k| self.tag_sets.get(&k));
939
952
  let def = set.and_then(|s| s.tags.iter().find(|d| d.id == t.tag_index));
953
+ let tag = def
954
+ .map(|d| d.name.clone())
955
+ .unwrap_or_else(|| format!("tag_{}", t.tag_index));
956
+ let set_name = set.and_then(|s| s.name.clone());
940
957
  let [ox, oy, oz] = t.origin;
941
958
  let [dx, dy, dz] = t.offset;
942
959
  let has_pos = t.origin != [0.0; 3] || t.offset != [0.0; 3];
960
+ // Observed: hidden tags carry 0x0080 in the property word (FORMAT_NOTES).
961
+ let invisible = h.properties & 0x0080 != 0;
962
+ let mut props = Props::new();
963
+ let mut layer = None;
964
+ if h.has_display_header {
965
+ layer = Some(self.level_name(h.level));
966
+ props.insert("dgn.color_index".into(), Value::Int(i64::from(h.color)));
967
+ props.insert("dgn.weight".into(), Value::Int(i64::from(h.weight)));
968
+ props.insert("dgn.style".into(), Value::Int(i64::from(h.style)));
969
+ if h.graphic_group != 0 {
970
+ props.insert(
971
+ "dgn.graphic_group".into(),
972
+ Value::Int(i64::from(h.graphic_group)),
973
+ );
974
+ }
975
+ // Only MicroStation's undecoded property bits add anything to `invisible`.
976
+ if h.properties & 0x0600 != 0 {
977
+ props.insert("dgn.properties".into(), Value::Int(i64::from(h.properties)));
978
+ }
979
+ if h.type_flags & 0x00c0 != 0 {
980
+ props.insert(
981
+ "dgn.type_flags".into(),
982
+ Value::Int(i64::from(h.type_flags & 0x00c0)),
983
+ );
984
+ }
985
+ }
986
+ // V7 tags and hidden V8 tags without a text size have no usable presentation.
987
+ let display = (t.size[1] > 0.0 && t.size[0] > 0.0).then(|| {
988
+ self.used_fonts.insert(t.font);
989
+ props.insert("dgn.font_number".into(), Value::Int(i64::from(t.font)));
990
+ props.insert(
991
+ "dgn.justification".into(),
992
+ Value::Int(i64::from(t.justification)),
993
+ );
994
+ let (halign, valign, _, _) = justification(t.justification);
995
+ AttributeDisplay {
996
+ height: xf.dist(t.size[1]),
997
+ width: xf.dist(t.size[0]),
998
+ style: Some(self.font_style_name(t.font)),
999
+ halign,
1000
+ valign,
1001
+ rotation: tag_rotation(t.quaternion),
1002
+ }
1003
+ });
943
1004
  Attribute {
944
- tag: def
945
- .map(|d| d.name.clone())
946
- .unwrap_or_else(|| format!("tag_{}", t.tag_index)),
1005
+ tag,
947
1006
  value: tag_value(&t.value),
948
- set: set.and_then(|s| s.name.clone()),
1007
+ set: set_name,
949
1008
  position: has_pos.then(|| xf.pt([ox + dx, oy + dy, oz + dz])),
950
- // Observed: hidden tags carry 0x0080 in the property word (FORMAT_NOTES).
951
- invisible: h.properties & 0x0080 != 0,
1009
+ invisible,
1010
+ layer,
1011
+ display,
1012
+ props,
952
1013
  }
953
1014
  }
954
1015
  }
@@ -1060,29 +1121,63 @@ fn curve(points: &[UorPoint], xf: &Xf) -> EntityKind {
1060
1121
  }
1061
1122
  }
1062
1123
 
1124
+ /// Pixel X and Y axes of a raster frame matrix (row-major 4x4): its first two columns,
1125
+ /// one pixel long each, in UOR.
1126
+ pub(crate) fn frame_axes(m: &[f64; 16]) -> (Vec3, Vec3) {
1127
+ let [m00, m01, _, _, m10, m11, _, _, m20, m21, ..] = *m;
1128
+ (Vec3::new(m00, m10, m20), Vec3::new(m01, m11, m21))
1129
+ }
1130
+
1063
1131
  /// Raster frame placement from its pixel-to-UOR matrix (row-major, translation in the
1064
- /// last column): the image starts at the translation and its edges follow the matrix X / Y
1065
- /// columns, scaled so that the frame reaches its stored extent corner. Handles rotated and
1066
- /// sheared frames; `None` when the corner is missing or the system is degenerate.
1132
+ /// last column) and its edge lengths (`0x108`/`0x110`, UOR): the image starts at the
1133
+ /// translation (its lower-left corner) and its edges run along the unit matrix X / Y
1134
+ /// columns for those lengths, `u = â·ext_x`, `v = b̂·ext_y`. The lengths are measured
1135
+ /// along the image axes (width·|a| and height·|b|), not as an XY extent, so rotated and
1136
+ /// sheared frames need no solving. `None` when an axis or a length is degenerate.
1067
1137
  pub(crate) fn frame_placement(
1068
1138
  m: &[f64; 16],
1069
- corner: Option<[f64; 2]>,
1139
+ edges: Option<[f64; 2]>,
1070
1140
  ) -> Option<(UorPoint, Vec3, Vec3)> {
1071
- let [m00, m01, _, m03, m10, m11, _, m13, m20, m21, _, m23, ..] = *m;
1072
- let a = Vec3::new(m00, m10, m20);
1073
- let b = Vec3::new(m01, m11, m21);
1074
- let [cx, cy] = corner?;
1075
- let (dx, dy) = (cx - m03, cy - m13);
1076
- let det = a.x * b.y - a.y * b.x;
1077
- if !det.is_finite() || det.abs() <= 1e-12 * a.length() * b.length() {
1141
+ let [_, _, _, m03, _, _, _, m13, _, _, _, m23, ..] = *m;
1142
+ let (a, b) = frame_axes(m);
1143
+ let [ex, ey] = edges?;
1144
+ if !(ex.is_finite() && ey.is_finite() && ex > 0.0 && ey > 0.0) {
1078
1145
  return None;
1079
1146
  }
1080
- let w = (dx * b.y - dy * b.x) / det;
1081
- let h = (a.x * dy - a.y * dx) / det;
1082
- if !(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0) {
1147
+ let (ua, ub) = (a.normalized()?, b.normalized()?);
1148
+ // Parallel axes span no rectangle.
1149
+ if ua.cross(ub).length() <= 1e-9 {
1150
+ return None;
1151
+ }
1152
+ if ![m03, m13, m23].iter().all(|v| v.is_finite()) {
1153
+ return None;
1154
+ }
1155
+ Some(([m03, m13, m23], ua.scaled(ex), ub.scaled(ey)))
1156
+ }
1157
+
1158
+ /// Pixel size of a raster frame: its edge lengths over the pixel lengths `|a|` and `|b|`
1159
+ /// of the matrix axes, rounded. `None` when that is not a positive `u32`.
1160
+ pub(crate) fn frame_pixels(m: &[f64; 16], edges: Option<[f64; 2]>) -> Option<[u32; 2]> {
1161
+ let (a, b) = frame_axes(m);
1162
+ let [ex, ey] = edges?;
1163
+ let count = |edge: f64, axis: Vec3| {
1164
+ let n = (edge / axis.length()).round();
1165
+ (n.is_finite() && n >= 1.0 && n <= f64::from(u32::MAX)).then_some(n as u32)
1166
+ };
1167
+ Some([count(ex, a)?, count(ey, b)?])
1168
+ }
1169
+
1170
+ /// Rotation of the image X axis and skew of its Y axis from the perpendicular, both in
1171
+ /// the XY plane, in radians within (-π, π].
1172
+ fn frame_angles(m: &[f64; 16]) -> Option<(f64, f64)> {
1173
+ let (a, b) = frame_axes(m);
1174
+ if a.normalized().is_none() || b.normalized().is_none() {
1083
1175
  return None;
1084
1176
  }
1085
- Some(([m03, m13, m23], a.scaled(w), b.scaled(h)))
1177
+ let rotation = a.y.atan2(a.x);
1178
+ let skew = b.y.atan2(b.x) - rotation - FRAC_PI_2;
1179
+ let skew = skew.sin().atan2(skew.cos());
1180
+ (rotation.is_finite() && skew.is_finite()).then_some((rotation, skew))
1086
1181
  }
1087
1182
 
1088
1183
  /// Most XAttribute kinds listed on one entity (the count is always complete).
@@ -1154,6 +1249,28 @@ pub(crate) fn justification(code: u16) -> (HAlign, VAlign, f64, f64) {
1154
1249
  (h, v, fx, fy)
1155
1250
  }
1156
1251
 
1252
+ /// Keeps only what a tag element does not share with its owner: `layer` stays `None` on the
1253
+ /// owner's level, and symbology/flag props equal to the owner's are dropped.
1254
+ fn relative_to_owner(mut a: Attribute, owner: &Entity) -> Attribute {
1255
+ if a.layer.is_some() && a.layer == owner.layer {
1256
+ a.layer = None;
1257
+ }
1258
+ a.props
1259
+ .retain(|key, value| owner.props.get(key) != Some(value) || key == "dgn.font_number");
1260
+ a
1261
+ }
1262
+
1263
+ /// Rotation of a displayed tag about +Z in radians, from its stored quaternion (same
1264
+ /// convention as text, see [`Rotation::matrix`]). An all-zero quaternion means none.
1265
+ pub(crate) fn tag_rotation(q: [f64; 4]) -> f64 {
1266
+ if q.iter().all(|v| *v == 0.0) {
1267
+ return 0.0;
1268
+ }
1269
+ let [m00, _, _, m10, ..] = Rotation::Quaternion(q).matrix();
1270
+ let a = m10.atan2(m00);
1271
+ if a.abs() < 1e-12 { 0.0 } else { a }
1272
+ }
1273
+
1157
1274
  /// Text length in UOR and whether it is estimated (`characters x character width`, the
1158
1275
  /// estimate GDAL dgnlib's writer uses for text bounds).
1159
1276
  fn text_length(t: &TextData) -> (f64, bool) {
@@ -1457,25 +1574,46 @@ mod tests {
1457
1574
 
1458
1575
  #[test]
1459
1576
  fn raster_placement_follows_the_matrix() {
1460
- // 30 degree rotation, 2 UOR per pixel, translation (100, 50); a 300 x 200 pixel image.
1577
+ // 30 degree rotation, 2 UOR per pixel, translation (100, 50); a 300 x 200 pixel
1578
+ // image. `0x108`/`0x110` hold the edge lengths along the image axes (W·|a|, H·|b|).
1461
1579
  let (s, c) = 30f64.to_radians().sin_cos();
1462
1580
  let (a, b) = ([2.0 * c, 2.0 * s], [-2.0 * s, 2.0 * c]);
1463
1581
  let m = [
1464
1582
  a[0], b[0], 0.0, 100.0, a[1], b[1], 0.0, 50.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1465
1583
  ];
1466
- let corner = [
1467
- 100.0 + 300.0 * a[0] + 200.0 * b[0],
1468
- 50.0 + 300.0 * a[1] + 200.0 * b[1],
1469
- ];
1470
- let (o, u, v) = frame_placement(&m, Some(corner)).unwrap();
1584
+ let edges = [600.0, 400.0];
1585
+ let (o, u, v) = frame_placement(&m, Some(edges)).unwrap();
1471
1586
  assert_eq!(o, [100.0, 50.0, 0.0]);
1472
1587
  assert!(close(u.x, 600.0 * c) && close(u.y, 600.0 * s));
1473
1588
  assert!(close(v.x, -400.0 * s) && close(v.y, 400.0 * c));
1589
+ assert_eq!(frame_pixels(&m, Some(edges)), Some([300, 200]));
1590
+ let (rotation, skew) = frame_angles(&m).unwrap();
1591
+ assert!(close(rotation, 30f64.to_radians()) && close(skew, 0.0));
1474
1592
  assert!(frame_placement(&m, None).is_none());
1593
+ assert!(frame_placement(&m, Some([0.0, 400.0])).is_none());
1475
1594
  let mut flat = m;
1476
1595
  flat[1] = flat[0];
1477
1596
  flat[5] = flat[4];
1478
- assert!(frame_placement(&flat, Some(corner)).is_none(), "degenerate");
1597
+ assert!(frame_placement(&flat, Some(edges)).is_none(), "degenerate");
1598
+ }
1599
+
1600
+ #[test]
1601
+ fn sheared_raster_frames_keep_their_axes() {
1602
+ // X axis 3 UOR per pixel along +X; Y axis 2 UOR per pixel tilted 10 degrees left of
1603
+ // +Y. The Y edge runs along the tilted axis for H·|b| UOR.
1604
+ let (s, c) = 10f64.to_radians().sin_cos();
1605
+ let (a, b) = ([3.0, 0.0], [-2.0 * s, 2.0 * c]);
1606
+ let m = [
1607
+ a[0], b[0], 0.0, -5.0, a[1], b[1], 0.0, 7.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1608
+ ];
1609
+ let edges = [3.0 * 120.0, 2.0 * 80.0];
1610
+ let (o, u, v) = frame_placement(&m, Some(edges)).unwrap();
1611
+ assert_eq!(o, [-5.0, 7.0, 0.0]);
1612
+ assert!(close(u.x, 360.0) && close(u.y, 0.0));
1613
+ assert!(close(v.x, -160.0 * s) && close(v.y, 160.0 * c));
1614
+ assert_eq!(frame_pixels(&m, Some(edges)), Some([120, 80]));
1615
+ let (rotation, skew) = frame_angles(&m).unwrap();
1616
+ assert!(close(rotation, 0.0) && close(skew, 10f64.to_radians()));
1479
1617
  }
1480
1618
 
1481
1619
  #[test]
@@ -203,48 +203,6 @@ fn map_file(file: &V8File, options: &ReadOptions, enc: &'static encoding_rs::Enc
203
203
  enc,
204
204
  options,
205
205
  );
206
- // Frames that store the same extent corner for the same file: their extents are then
207
- // not image footprints (private sample: 8 frames, one file, one corner).
208
- m.shared_raster_extent.clear();
209
- let mut by_extent: HashMap<(i64, i64, Option<String>), Vec<u64>> = HashMap::new();
210
- for it in &graphic {
211
- if let (
212
- ElementData::RasterFrame {
213
- corner: Some([cx, cy]),
214
- ..
215
- },
216
- Some(id),
217
- ) = (&it.el.data, it.el.header.id)
218
- {
219
- let file = m
220
- .raster_paths
221
- .get(&id)
222
- .and_then(|(n, f)| f.clone().or_else(|| n.clone()));
223
- by_extent
224
- .entry((cx.round() as i64, cy.round() as i64, file))
225
- .or_default()
226
- .push(id);
227
- }
228
- }
229
- let shared: Vec<u64> = by_extent
230
- .into_values()
231
- .filter(|v| v.len() > 1)
232
- .flatten()
233
- .collect();
234
- if !shared.is_empty() {
235
- m.warn(
236
- "dgn.raster_extent_shared",
237
- format!(
238
- "{} raster frames share one stored extent corner and file; their \
239
- rectangles (as stored, equal to the element ranges) are probably not \
240
- image footprints",
241
- shared.len()
242
- ),
243
- None,
244
- None,
245
- );
246
- m.shared_raster_extent.extend(shared);
247
- }
248
206
  m.pending_tags.clear();
249
207
  m.attached.clear();
250
208
  for (i, it) in graphic.iter().enumerate() {