skppy 0.9.0__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 (300) hide show
  1. skppy-0.9.0/.agents/CODE_STYLE.md +485 -0
  2. skppy-0.9.0/.agents/README.md +46 -0
  3. skppy-0.9.0/.agents/TESTING.md +35 -0
  4. skppy-0.9.0/.github/workflows/blender-addon.yml +116 -0
  5. skppy-0.9.0/.github/workflows/code-quality.yml +79 -0
  6. skppy-0.9.0/.github/workflows/docs.yml +61 -0
  7. skppy-0.9.0/.github/workflows/publish.yml +199 -0
  8. skppy-0.9.0/.github/workflows/test.yml +181 -0
  9. skppy-0.9.0/.gitignore +77 -0
  10. skppy-0.9.0/CHANGELOG.md +106 -0
  11. skppy-0.9.0/LICENSE +21 -0
  12. skppy-0.9.0/Makefile +41 -0
  13. skppy-0.9.0/PKG-INFO +159 -0
  14. skppy-0.9.0/README.md +125 -0
  15. skppy-0.9.0/blender_skp_io/__init__.py +58 -0
  16. skppy-0.9.0/blender_skp_io/annotation_builder.py +289 -0
  17. skppy-0.9.0/blender_skp_io/blender_manifest.toml +18 -0
  18. skppy-0.9.0/blender_skp_io/export_builder.py +802 -0
  19. skppy-0.9.0/blender_skp_io/operators/__init__.py +5 -0
  20. skppy-0.9.0/blender_skp_io/operators/export_skp.py +167 -0
  21. skppy-0.9.0/blender_skp_io/operators/import_skp.py +330 -0
  22. skppy-0.9.0/blender_skp_io/scene_builder.py +1143 -0
  23. skppy-0.9.0/build_blender_addon.py +300 -0
  24. skppy-0.9.0/docs/Makefile +28 -0
  25. skppy-0.9.0/docs/README.md +32 -0
  26. skppy-0.9.0/docs/_static/.gitkeep +0 -0
  27. skppy-0.9.0/docs/_templates/autosummary/module.rst +6 -0
  28. skppy-0.9.0/docs/api/index.rst +53 -0
  29. skppy-0.9.0/docs/blender/architecture.md +282 -0
  30. skppy-0.9.0/docs/blender/building.md +106 -0
  31. skppy-0.9.0/docs/blender/export_options.md +140 -0
  32. skppy-0.9.0/docs/blender/import_options.md +241 -0
  33. skppy-0.9.0/docs/blender/index.md +37 -0
  34. skppy-0.9.0/docs/blender/installing.md +81 -0
  35. skppy-0.9.0/docs/blender/scene_organization.md +124 -0
  36. skppy-0.9.0/docs/conf.py +90 -0
  37. skppy-0.9.0/docs/format/edge_shading.md +100 -0
  38. skppy-0.9.0/docs/format/index.md +30 -0
  39. skppy-0.9.0/docs/format/old_format.md +126 -0
  40. skppy-0.9.0/docs/format/reference/legacy_classes.md +122 -0
  41. skppy-0.9.0/docs/format/reference/legacy_container.md +140 -0
  42. skppy-0.9.0/docs/format/reference/legacy_fields.md +893 -0
  43. skppy-0.9.0/docs/format/skp_format.md +558 -0
  44. skppy-0.9.0/docs/format/uv_projection.md +227 -0
  45. skppy-0.9.0/docs/getting_started.md +195 -0
  46. skppy-0.9.0/docs/guides/components.md +201 -0
  47. skppy-0.9.0/docs/guides/errors.md +160 -0
  48. skppy-0.9.0/docs/guides/examples.md +65 -0
  49. skppy-0.9.0/docs/guides/geometry.md +177 -0
  50. skppy-0.9.0/docs/guides/index.md +25 -0
  51. skppy-0.9.0/docs/guides/materials.md +176 -0
  52. skppy-0.9.0/docs/guides/reading.md +186 -0
  53. skppy-0.9.0/docs/guides/writing.md +270 -0
  54. skppy-0.9.0/docs/index.rst +51 -0
  55. skppy-0.9.0/docs/installing.md +87 -0
  56. skppy-0.9.0/docs/requirements.txt +7 -0
  57. skppy-0.9.0/docs/skp_tags.yaml +1418 -0
  58. skppy-0.9.0/pyproject.toml +69 -0
  59. skppy-0.9.0/ruff.toml +25 -0
  60. skppy-0.9.0/setup.cfg +4 -0
  61. skppy-0.9.0/skppy/__init__.py +269 -0
  62. skppy-0.9.0/skppy/_cancellation.py +32 -0
  63. skppy-0.9.0/skppy/_version.py +24 -0
  64. skppy-0.9.0/skppy/coplanar.py +146 -0
  65. skppy-0.9.0/skppy/data_structure/__init__.py +149 -0
  66. skppy-0.9.0/skppy/data_structure/annotations.py +128 -0
  67. skppy-0.9.0/skppy/data_structure/construction.py +264 -0
  68. skppy-0.9.0/skppy/data_structure/document.py +143 -0
  69. skppy-0.9.0/skppy/data_structure/entities.py +1254 -0
  70. skppy-0.9.0/skppy/data_structure/header.py +80 -0
  71. skppy-0.9.0/skppy/data_structure/images.py +72 -0
  72. skppy-0.9.0/skppy/data_structure/layers.py +93 -0
  73. skppy-0.9.0/skppy/data_structure/materials.py +102 -0
  74. skppy-0.9.0/skppy/data_structure/mesh_indexing.py +125 -0
  75. skppy-0.9.0/skppy/data_structure/mesh_preparation.py +243 -0
  76. skppy-0.9.0/skppy/data_structure/meta.py +51 -0
  77. skppy-0.9.0/skppy/data_structure/model.py +484 -0
  78. skppy-0.9.0/skppy/data_structure/model_metadata.py +858 -0
  79. skppy-0.9.0/skppy/data_structure/primitives.py +476 -0
  80. skppy-0.9.0/skppy/data_structure/scene.py +371 -0
  81. skppy-0.9.0/skppy/data_structure/scene_data.py +112 -0
  82. skppy-0.9.0/skppy/data_structure/scene_graph.py +109 -0
  83. skppy-0.9.0/skppy/exceptions.py +100 -0
  84. skppy-0.9.0/skppy/legacy_writter/__init__.py +6 -0
  85. skppy-0.9.0/skppy/legacy_writter/envelope.py +311 -0
  86. skppy-0.9.0/skppy/legacy_writter/extensions.py +299 -0
  87. skppy-0.9.0/skppy/legacy_writter/model.py +1368 -0
  88. skppy-0.9.0/skppy/loader.py +187 -0
  89. skppy-0.9.0/skppy/parser/__init__.py +35 -0
  90. skppy-0.9.0/skppy/parser/attributes.py +111 -0
  91. skppy-0.9.0/skppy/parser/background_images.py +102 -0
  92. skppy-0.9.0/skppy/parser/camera_parser.py +83 -0
  93. skppy-0.9.0/skppy/parser/definitions.py +128 -0
  94. skppy-0.9.0/skppy/parser/entities.py +1049 -0
  95. skppy-0.9.0/skppy/parser/header_parser.py +317 -0
  96. skppy-0.9.0/skppy/parser/layers.py +221 -0
  97. skppy-0.9.0/skppy/parser/material_parser.py +545 -0
  98. skppy-0.9.0/skppy/parser/meta_parser.py +220 -0
  99. skppy-0.9.0/skppy/parser/model_parser.py +708 -0
  100. skppy-0.9.0/skppy/parser/rendering_options.py +175 -0
  101. skppy-0.9.0/skppy/parser/scenes_parser.py +181 -0
  102. skppy-0.9.0/skppy/parser/tlv.py +1123 -0
  103. skppy-0.9.0/skppy/parser/vray_materials.py +185 -0
  104. skppy-0.9.0/skppy/parser/zip_entries.py +65 -0
  105. skppy-0.9.0/skppy/parser_legacy/__init__.py +57 -0
  106. skppy-0.9.0/skppy/parser_legacy/annotation_payloads.py +80 -0
  107. skppy-0.9.0/skppy/parser_legacy/annotation_readers.py +178 -0
  108. skppy-0.9.0/skppy/parser_legacy/attribute_builder.py +47 -0
  109. skppy-0.9.0/skppy/parser_legacy/attribute_payloads.py +101 -0
  110. skppy-0.9.0/skppy/parser_legacy/attribute_readers.py +102 -0
  111. skppy-0.9.0/skppy/parser_legacy/base_payloads.py +192 -0
  112. skppy-0.9.0/skppy/parser_legacy/binary.py +370 -0
  113. skppy-0.9.0/skppy/parser_legacy/camera_payloads.py +109 -0
  114. skppy-0.9.0/skppy/parser_legacy/class_support.py +16 -0
  115. skppy-0.9.0/skppy/parser_legacy/colors.py +10 -0
  116. skppy-0.9.0/skppy/parser_legacy/component_body.py +109 -0
  117. skppy-0.9.0/skppy/parser_legacy/component_builder.py +104 -0
  118. skppy-0.9.0/skppy/parser_legacy/component_payloads.py +104 -0
  119. skppy-0.9.0/skppy/parser_legacy/component_readers.py +126 -0
  120. skppy-0.9.0/skppy/parser_legacy/diagnostics.py +293 -0
  121. skppy-0.9.0/skppy/parser_legacy/entity_builder.py +594 -0
  122. skppy-0.9.0/skppy/parser_legacy/envelope.py +95 -0
  123. skppy-0.9.0/skppy/parser_legacy/errors.py +70 -0
  124. skppy-0.9.0/skppy/parser_legacy/extensions.py +258 -0
  125. skppy-0.9.0/skppy/parser_legacy/geometry_payloads.py +200 -0
  126. skppy-0.9.0/skppy/parser_legacy/geometry_readers.py +582 -0
  127. skppy-0.9.0/skppy/parser_legacy/image_payloads.py +180 -0
  128. skppy-0.9.0/skppy/parser_legacy/layer_payloads.py +237 -0
  129. skppy-0.9.0/skppy/parser_legacy/line_style_payloads.py +47 -0
  130. skppy-0.9.0/skppy/parser_legacy/material_builder.py +89 -0
  131. skppy-0.9.0/skppy/parser_legacy/material_payloads.py +136 -0
  132. skppy-0.9.0/skppy/parser_legacy/metadata_payloads.py +278 -0
  133. skppy-0.9.0/skppy/parser_legacy/metadata_readers.py +115 -0
  134. skppy-0.9.0/skppy/parser_legacy/model_builder.py +229 -0
  135. skppy-0.9.0/skppy/parser_legacy/model_tail.py +143 -0
  136. skppy-0.9.0/skppy/parser_legacy/object_dispatch.py +124 -0
  137. skppy-0.9.0/skppy/parser_legacy/object_readers.py +350 -0
  138. skppy-0.9.0/skppy/parser_legacy/options_payloads.py +90 -0
  139. skppy-0.9.0/skppy/parser_legacy/parser.py +468 -0
  140. skppy-0.9.0/skppy/parser_legacy/parser_types.py +413 -0
  141. skppy-0.9.0/skppy/parser_legacy/provenance.py +138 -0
  142. skppy-0.9.0/skppy/parser_legacy/read_context.py +107 -0
  143. skppy-0.9.0/skppy/parser_legacy/recovery.py +448 -0
  144. skppy-0.9.0/skppy/parser_legacy/relationship_payloads.py +55 -0
  145. skppy-0.9.0/skppy/parser_legacy/rendering_options.py +177 -0
  146. skppy-0.9.0/skppy/parser_legacy/root_component.py +14 -0
  147. skppy-0.9.0/skppy/parser_legacy/root_payloads.py +23 -0
  148. skppy-0.9.0/skppy/parser_legacy/scene_builder.py +58 -0
  149. skppy-0.9.0/skppy/parser_legacy/scene_pages.py +382 -0
  150. skppy-0.9.0/skppy/parser_legacy/schema.py +199 -0
  151. skppy-0.9.0/skppy/parser_legacy/session.py +63 -0
  152. skppy-0.9.0/skppy/parser_legacy/uv_payloads.py +69 -0
  153. skppy-0.9.0/skppy/parser_legacy/visual_payloads.py +185 -0
  154. skppy-0.9.0/skppy/triangulation.py +1072 -0
  155. skppy-0.9.0/skppy/utils.py +117 -0
  156. skppy-0.9.0/skppy/writer/__init__.py +40 -0
  157. skppy-0.9.0/skppy/writer/annotation_styles.py +139 -0
  158. skppy-0.9.0/skppy/writer/attributes.py +89 -0
  159. skppy-0.9.0/skppy/writer/background_images.py +140 -0
  160. skppy-0.9.0/skppy/writer/cameras.py +127 -0
  161. skppy-0.9.0/skppy/writer/container.py +78 -0
  162. skppy-0.9.0/skppy/writer/definitions.py +148 -0
  163. skppy-0.9.0/skppy/writer/entities.py +1531 -0
  164. skppy-0.9.0/skppy/writer/environments.py +153 -0
  165. skppy-0.9.0/skppy/writer/fonts.py +56 -0
  166. skppy-0.9.0/skppy/writer/layers.py +229 -0
  167. skppy-0.9.0/skppy/writer/line_styles.py +132 -0
  168. skppy-0.9.0/skppy/writer/materials.py +219 -0
  169. skppy-0.9.0/skppy/writer/model_data.py +567 -0
  170. skppy-0.9.0/skppy/writer/model_metadata.py +205 -0
  171. skppy-0.9.0/skppy/writer/options.py +58 -0
  172. skppy-0.9.0/skppy/writer/scenes.py +202 -0
  173. skppy-0.9.0/skppy/writer/styles.py +306 -0
  174. skppy-0.9.0/skppy/writer/sun_data.py +19 -0
  175. skppy-0.9.0/skppy/writer/tlv.py +34 -0
  176. skppy-0.9.0/skppy/writer/vray_materials.py +292 -0
  177. skppy-0.9.0/skppy/writer/watermarks.py +119 -0
  178. skppy-0.9.0/skppy.egg-info/PKG-INFO +159 -0
  179. skppy-0.9.0/skppy.egg-info/SOURCES.txt +298 -0
  180. skppy-0.9.0/skppy.egg-info/dependency_links.txt +1 -0
  181. skppy-0.9.0/skppy.egg-info/requires.txt +14 -0
  182. skppy-0.9.0/skppy.egg-info/scm_file_list.json +294 -0
  183. skppy-0.9.0/skppy.egg-info/scm_version.json +8 -0
  184. skppy-0.9.0/skppy.egg-info/top_level.txt +1 -0
  185. skppy-0.9.0/tests/__init__.py +2 -0
  186. skppy-0.9.0/tests/blender/README.md +29 -0
  187. skppy-0.9.0/tests/blender/__init__.py +2 -0
  188. skppy-0.9.0/tests/blender/fixture_data.py +67 -0
  189. skppy-0.9.0/tests/blender/run_integration.py +763 -0
  190. skppy-0.9.0/tests/legacy_format/README.md +22 -0
  191. skppy-0.9.0/tests/legacy_format/__init__.py +2 -0
  192. skppy-0.9.0/tests/legacy_format/_fixtures.py +1076 -0
  193. skppy-0.9.0/tests/legacy_format/test_annotations.py +250 -0
  194. skppy-0.9.0/tests/legacy_format/test_archive.py +361 -0
  195. skppy-0.9.0/tests/legacy_format/test_auxiliary_objects.py +352 -0
  196. skppy-0.9.0/tests/legacy_format/test_builder_dispatch_boundaries.py +441 -0
  197. skppy-0.9.0/tests/legacy_format/test_diagnostic_parser_boundaries.py +125 -0
  198. skppy-0.9.0/tests/legacy_format/test_diagnostics.py +326 -0
  199. skppy-0.9.0/tests/legacy_format/test_extensions.py +325 -0
  200. skppy-0.9.0/tests/legacy_format/test_geometry.py +310 -0
  201. skppy-0.9.0/tests/legacy_format/test_geometry_adapter_boundaries.py +236 -0
  202. skppy-0.9.0/tests/legacy_format/test_model_assembly.py +456 -0
  203. skppy-0.9.0/tests/legacy_format/test_model_loading.py +443 -0
  204. skppy-0.9.0/tests/legacy_format/test_object_dispatch.py +928 -0
  205. skppy-0.9.0/tests/legacy_format/test_options_payloads.py +69 -0
  206. skppy-0.9.0/tests/legacy_format/test_payload_boundaries.py +272 -0
  207. skppy-0.9.0/tests/legacy_format/test_recovery_boundaries.py +99 -0
  208. skppy-0.9.0/tests/legacy_format/test_resource_payload_boundaries.py +440 -0
  209. skppy-0.9.0/tests/legacy_format/test_root_camera_versions.py +55 -0
  210. skppy-0.9.0/tests/legacy_format/test_scene_tail_boundaries.py +277 -0
  211. skppy-0.9.0/tests/legacy_format/test_scenes_metadata.py +491 -0
  212. skppy-0.9.0/tests/legacy_format/test_versioned_payloads.py +230 -0
  213. skppy-0.9.0/tests/legacy_writter/__init__.py +2 -0
  214. skppy-0.9.0/tests/legacy_writter/test_arc_raw_payload.py +39 -0
  215. skppy-0.9.0/tests/legacy_writter/test_attributes.py +111 -0
  216. skppy-0.9.0/tests/legacy_writter/test_background_images.py +71 -0
  217. skppy-0.9.0/tests/legacy_writter/test_cameras.py +46 -0
  218. skppy-0.9.0/tests/legacy_writter/test_components.py +94 -0
  219. skppy-0.9.0/tests/legacy_writter/test_construction.py +74 -0
  220. skppy-0.9.0/tests/legacy_writter/test_curves.py +142 -0
  221. skppy-0.9.0/tests/legacy_writter/test_definition_payloads.py +14 -0
  222. skppy-0.9.0/tests/legacy_writter/test_dimensions.py +130 -0
  223. skppy-0.9.0/tests/legacy_writter/test_envelope.py +46 -0
  224. skppy-0.9.0/tests/legacy_writter/test_environments.py +102 -0
  225. skppy-0.9.0/tests/legacy_writter/test_extensions.py +51 -0
  226. skppy-0.9.0/tests/legacy_writter/test_fonts.py +85 -0
  227. skppy-0.9.0/tests/legacy_writter/test_layer_folders.py +38 -0
  228. skppy-0.9.0/tests/legacy_writter/test_line_styles.py +57 -0
  229. skppy-0.9.0/tests/legacy_writter/test_material_pbr.py +17 -0
  230. skppy-0.9.0/tests/legacy_writter/test_model_metadata.py +57 -0
  231. skppy-0.9.0/tests/legacy_writter/test_options.py +40 -0
  232. skppy-0.9.0/tests/legacy_writter/test_relationships.py +49 -0
  233. skppy-0.9.0/tests/legacy_writter/test_rendering_options.py +40 -0
  234. skppy-0.9.0/tests/legacy_writter/test_resources.py +111 -0
  235. skppy-0.9.0/tests/legacy_writter/test_scenes.py +143 -0
  236. skppy-0.9.0/tests/legacy_writter/test_section_planes.py +24 -0
  237. skppy-0.9.0/tests/legacy_writter/test_shadow_info.py +13 -0
  238. skppy-0.9.0/tests/legacy_writter/test_styles.py +117 -0
  239. skppy-0.9.0/tests/legacy_writter/test_texts.py +80 -0
  240. skppy-0.9.0/tests/legacy_writter/test_uv.py +47 -0
  241. skppy-0.9.0/tests/legacy_writter/test_vray_materials.py +82 -0
  242. skppy-0.9.0/tests/test_attribute_parser.py +127 -0
  243. skppy-0.9.0/tests/test_background_image_parser.py +143 -0
  244. skppy-0.9.0/tests/test_build_blender_addon.py +104 -0
  245. skppy-0.9.0/tests/test_coplanar.py +76 -0
  246. skppy-0.9.0/tests/test_data_structure_boundaries.py +242 -0
  247. skppy-0.9.0/tests/test_definition_parser.py +115 -0
  248. skppy-0.9.0/tests/test_document.py +64 -0
  249. skppy-0.9.0/tests/test_entity_parser.py +224 -0
  250. skppy-0.9.0/tests/test_header_parser.py +107 -0
  251. skppy-0.9.0/tests/test_layer_parser.py +137 -0
  252. skppy-0.9.0/tests/test_loader.py +140 -0
  253. skppy-0.9.0/tests/test_material_parser.py +249 -0
  254. skppy-0.9.0/tests/test_meta_parser.py +89 -0
  255. skppy-0.9.0/tests/test_model_builders.py +187 -0
  256. skppy-0.9.0/tests/test_model_parser.py +65 -0
  257. skppy-0.9.0/tests/test_modern_entities.py +279 -0
  258. skppy-0.9.0/tests/test_modern_entity_boundaries.py +371 -0
  259. skppy-0.9.0/tests/test_modern_model_boundaries.py +305 -0
  260. skppy-0.9.0/tests/test_modern_model_integration.py +406 -0
  261. skppy-0.9.0/tests/test_modern_parser_boundaries.py +246 -0
  262. skppy-0.9.0/tests/test_package_version.py +34 -0
  263. skppy-0.9.0/tests/test_parser_fixture_independence.py +37 -0
  264. skppy-0.9.0/tests/test_parser_mutations.py +144 -0
  265. skppy-0.9.0/tests/test_prepared_mesh.py +395 -0
  266. skppy-0.9.0/tests/test_publication_metadata.py +45 -0
  267. skppy-0.9.0/tests/test_scene_parser.py +102 -0
  268. skppy-0.9.0/tests/test_tlv.py +83 -0
  269. skppy-0.9.0/tests/test_triangulation.py +147 -0
  270. skppy-0.9.0/tests/test_triangulation_boundaries.py +154 -0
  271. skppy-0.9.0/tests/test_utils.py +217 -0
  272. skppy-0.9.0/tests/test_uv_projection.py +216 -0
  273. skppy-0.9.0/tests/test_vray_materials.py +246 -0
  274. skppy-0.9.0/tests/test_writer_annotation_styles.py +109 -0
  275. skppy-0.9.0/tests/test_writer_attributes.py +267 -0
  276. skppy-0.9.0/tests/test_writer_background_images.py +119 -0
  277. skppy-0.9.0/tests/test_writer_cameras.py +96 -0
  278. skppy-0.9.0/tests/test_writer_container.py +96 -0
  279. skppy-0.9.0/tests/test_writer_definitions.py +115 -0
  280. skppy-0.9.0/tests/test_writer_entities.py +591 -0
  281. skppy-0.9.0/tests/test_writer_environments.py +152 -0
  282. skppy-0.9.0/tests/test_writer_images.py +76 -0
  283. skppy-0.9.0/tests/test_writer_layers.py +116 -0
  284. skppy-0.9.0/tests/test_writer_legacy.py +185 -0
  285. skppy-0.9.0/tests/test_writer_line_styles.py +80 -0
  286. skppy-0.9.0/tests/test_writer_linear_dimensions.py +115 -0
  287. skppy-0.9.0/tests/test_writer_materials.py +264 -0
  288. skppy-0.9.0/tests/test_writer_model_data.py +429 -0
  289. skppy-0.9.0/tests/test_writer_model_metadata.py +93 -0
  290. skppy-0.9.0/tests/test_writer_options.py +81 -0
  291. skppy-0.9.0/tests/test_writer_public_api.py +75 -0
  292. skppy-0.9.0/tests/test_writer_radial_dimensions.py +80 -0
  293. skppy-0.9.0/tests/test_writer_scenes.py +241 -0
  294. skppy-0.9.0/tests/test_writer_styles.py +139 -0
  295. skppy-0.9.0/tests/test_writer_sun_data.py +29 -0
  296. skppy-0.9.0/tests/test_writer_texts.py +147 -0
  297. skppy-0.9.0/tests/test_writer_tlv.py +57 -0
  298. skppy-0.9.0/tests/test_writer_vray_materials.py +174 -0
  299. skppy-0.9.0/tests/test_writer_watermarks.py +113 -0
  300. skppy-0.9.0/tests/test_zip_entries.py +31 -0
@@ -0,0 +1,485 @@
1
+ # Code Style
2
+
3
+ This standard applies to all new and modified code in `skppy`, `blender_skp_io`, build scripts, and tests.
4
+
5
+ The keywords **must**, **should**, and **may** are intentional:
6
+
7
+ - **Must**: required; exceptions need a code comment or commit explanation.
8
+ - **Should**: the normal choice; deviate only when the alternative is clearer.
9
+ - **May**: optional and context-dependent.
10
+
11
+ ## Core principles
12
+
13
+ Code must be compact without becoming cryptic. Optimize for the next reader, not for the fewest characters.
14
+
15
+ - Keep behavior explicit, local, typed, and easy to test.
16
+ - Prefer straightforward data flow over clever abstractions or hidden state.
17
+ - Reject invalid data at the boundary where it becomes known.
18
+ - Preserve public compatibility unless the task intentionally changes the documented contract.
19
+ - Make the smallest coherent change and keep unrelated edits out of the commit.
20
+
21
+ ## Formatting
22
+
23
+ - The default maximum line length is 120 characters. Break earlier when a line contains multiple ideas.
24
+ - Ruff is authoritative for Python formatting. Do not hand-format against Ruff.
25
+ - Use four spaces, double quotes, trailing commas in multiline constructs, and one statement per line.
26
+ - Use parentheses for multiline expressions. Do not use backslash continuations.
27
+ - Separate top-level definitions with two blank lines and method definitions with one blank line.
28
+ - Do not align assignments or arguments with manual padding; it creates noisy diffs.
29
+
30
+ Preferred:
31
+
32
+ ```python
33
+ material_id = material_ids_by_archive_index.get(
34
+ drawing_element.material_tag.index or 0,
35
+ )
36
+ ```
37
+
38
+ Also preferred when it remains easy to scan:
39
+
40
+ ```python
41
+ material_id = material_ids_by_archive_index.get(drawing_element.material_tag.index or 0)
42
+ ```
43
+
44
+ Avoid packing separate operations onto one line:
45
+
46
+ ```python
47
+ material = read_material(reader); model.materials.append(material)
48
+ ```
49
+
50
+ ## File layout and imports
51
+
52
+ Python source files must normally use this order:
53
+
54
+ 1. Shebang, when executable.
55
+ 2. One-line SPDX identifier.
56
+ 3. Module docstring.
57
+ 4. `from __future__ import annotations`, when needed.
58
+ 5. Standard-library imports.
59
+ 6. Third-party imports.
60
+ 7. Local imports.
61
+ 8. Constants, types, classes, and functions.
62
+
63
+ ```python
64
+ #!/usr/bin/env python3
65
+ # SPDX-License-Identifier: MIT
66
+ """Generate one writer conformance fixture."""
67
+
68
+ from __future__ import annotations
69
+
70
+ import struct
71
+ from pathlib import Path
72
+
73
+ import numpy as np
74
+
75
+ from skppy.data_structure.model import Model
76
+ ```
77
+
78
+ - Imports must remain at module scope unless deferring an import prevents a real cycle or optional dependency failure.
79
+ - Import names directly when that improves clarity. Avoid wildcard imports outside deliberately centralized test
80
+ fixtures.
81
+ - Remove unused imports. Do not retain imports for undocumented side effects.
82
+ - The first non-shebang source line must be `# SPDX-License-Identifier: MIT`; C/C++ files use
83
+ `// SPDX-License-Identifier: MIT`.
84
+
85
+ ## Naming
86
+
87
+ - Modules, functions, methods, and variables use descriptive `snake_case`.
88
+ - Classes and type aliases use `PascalCase`.
89
+ - Constants use `UPPER_CASE`.
90
+ - Private implementation details start with one underscore.
91
+ - Boolean names should read as predicates: `is_visible`, `has_texture`, `should_retry`, `can_resolve`.
92
+ - Collections should describe their members: `layers_by_id`, `material_ids`, `pending_references`.
93
+ - Include units when ambiguity is possible: `offset_bytes`, `angle_radians`, `width_points`.
94
+ - Retain wire-format names when they are the clearest connection to verified format evidence.
95
+
96
+ Preferred:
97
+
98
+ ```python
99
+ materials_by_archive_index: dict[int, Material] = {}
100
+ payload_end_offset = reader.tell()
101
+ is_perspective = reader.read_bool()
102
+ ```
103
+
104
+ Avoid vague or encoded names:
105
+
106
+ ```python
107
+ d = {}
108
+ x2 = reader.tell()
109
+ flag = reader.read_bool()
110
+ ```
111
+
112
+ Single-letter names are acceptable only for conventional, tiny scopes such as `x`, `y`, `z`, or a short comprehension.
113
+
114
+ ## Types and data models
115
+
116
+ - Public APIs and nontrivial internal boundaries must be typed.
117
+ - Prefer precise unions and protocols over `Any`. Use `Any` only at a genuinely dynamic boundary.
118
+ - Use `T | None`, built-in generics such as `list[int]`, and postponed annotations.
119
+ - Use dataclasses for records with named fields. Use tuples only for small, stable, self-evident groupings.
120
+ - Use frozen dataclasses for immutable parse state and mutable dataclasses for public model objects that users edit.
121
+ - Do not use mutable default arguments. Use `field(default_factory=list)` or `None` plus explicit initialization.
122
+ - Narrow dynamic values with `isinstance()` before accessing type-specific fields.
123
+ - Do not use `cast()` to hide a runtime uncertainty that should be validated.
124
+
125
+ Preferred:
126
+
127
+ ```python
128
+ def resolve_material(material_id: int, materials: dict[int, Material]) -> Material | None:
129
+ return materials.get(material_id)
130
+ ```
131
+
132
+ Avoid:
133
+
134
+ ```python
135
+ def resolve_material(material_id, materials) -> Any:
136
+ return materials.get(material_id)
137
+ ```
138
+
139
+ Preferred mutable default:
140
+
141
+ ```python
142
+ @dataclass
143
+ class LayerFolder:
144
+ child_layer_ids: list[int] = field(default_factory=list)
145
+ ```
146
+
147
+ Avoid:
148
+
149
+ ```python
150
+ def collect_layers(layers: list[Layer] = []) -> list[Layer]:
151
+ return layers
152
+ ```
153
+
154
+ ## Functions and control flow
155
+
156
+ - A function should perform one coherent operation at one abstraction level.
157
+ - Prefer guard clauses and early returns over nested condition pyramids.
158
+ - Extract a helper when it names a real concept, removes duplication, or isolates a complex boundary.
159
+ - Do not extract one-line helpers that merely rename obvious syntax.
160
+ - Prefer keyword-only arguments when multiple adjacent values have the same type or meaning could be confused.
161
+ - Keep side effects visible in the function name and close to the owning object.
162
+ - Return a value instead of mutating an output argument unless identity preservation is part of the contract.
163
+ - Avoid boolean parameters that radically change behavior; separate functions are often clearer.
164
+
165
+ Preferred:
166
+
167
+ ```python
168
+ def resolve_object(handle: ArchiveObjectHandle) -> object | None:
169
+ if handle.kind == "null":
170
+ return None
171
+ if handle.kind == "object_ref":
172
+ return objects.get(handle.object_index)
173
+ return read_new_object(handle)
174
+ ```
175
+
176
+ Avoid:
177
+
178
+ ```python
179
+ def resolve_object(handle: ArchiveObjectHandle) -> object | None:
180
+ if handle.kind != "null":
181
+ if handle.kind == "object_ref":
182
+ return objects.get(handle.object_index)
183
+ else:
184
+ return read_new_object(handle)
185
+ else:
186
+ return None
187
+ ```
188
+
189
+ Use comprehensions for simple transformations, not for multi-step control flow.
190
+
191
+ Preferred:
192
+
193
+ ```python
194
+ visible_layers = [layer for layer in layers if layer.visible]
195
+ ```
196
+
197
+ Avoid:
198
+
199
+ ```python
200
+ results = [convert(value) for value in values if validate(value) and update_cache(value)]
201
+ ```
202
+
203
+ ## Exceptions and validation
204
+
205
+ - Validate external bytes, paths, schemas, indexes, and public arguments before relying on them.
206
+ - Raise the most specific useful exception with the failing value and context.
207
+ - Use `NotImplementedError` for recognized but unsupported format variants.
208
+ - Use `ValueError` for malformed values or inconsistent payloads.
209
+ - Use `EOFError` for truncated binary input.
210
+ - Never use bare `except:`. Catch only errors that can be handled at that layer.
211
+ - Preserve the original exception with `raise ... from exc` when translating error domains.
212
+ - Do not silently guess missing wire data or substitute modern defaults for absent historical fields.
213
+
214
+ Preferred:
215
+
216
+ ```python
217
+ if class_version not in {4, 6}:
218
+ raise NotImplementedError(f"CTexture version {class_version} is not decoded.")
219
+ ```
220
+
221
+ Avoid:
222
+
223
+ ```python
224
+ try:
225
+ return parse_texture(reader)
226
+ except Exception:
227
+ return Texture()
228
+ ```
229
+
230
+ Assertions are for programmer invariants, not user-controlled data validation.
231
+
232
+ ```python
233
+ registration = index_table.register_new_object_tag(tag)
234
+ assert registration is not None # The null/reference cases returned above.
235
+ ```
236
+
237
+ ## State, mutation, and dependencies
238
+
239
+ - Keep mutable state owned by the narrowest practical object.
240
+ - Do not add module globals that change during parsing, writing, or tests.
241
+ - Make mutation obvious: use verbs such as `append`, `register`, `populate`, `apply`, or `update`.
242
+ - Copy caller-owned collections only when isolation is required; otherwise document retained ownership.
243
+ - Avoid speculative caching. Add caches only with a measured or structurally clear benefit and an invalidation strategy.
244
+ - Reuse existing dependencies. A new runtime dependency requires a clear user benefit and maintenance justification.
245
+ - Keep pure-Python `skppy` independent from Blender; Blender-specific code belongs in `blender_skp_io`.
246
+
247
+ Preferred dependency direction:
248
+
249
+ ```text
250
+ blender_skp_io -> skppy -> Python standard library / NumPy
251
+ ```
252
+
253
+ Forbidden dependency direction:
254
+
255
+ ```text
256
+ skppy -> bpy or blender_skp_io
257
+ ```
258
+
259
+ ## Public APIs and compatibility
260
+
261
+ - Public names, signatures, defaults, serialized output, and model semantics are compatibility contracts.
262
+ - Add optional parameters as keyword-only unless positional use is intentionally supported.
263
+ - Keep internal archive indexes separate from public model IDs.
264
+ - Preserve object identity when references represent the same archived object.
265
+ - Document intentional breaking changes and update tests and user documentation in the same commit series.
266
+ - Do not expose parser-only state through public model classes when provenance can retain it privately.
267
+
268
+ Preferred extension:
269
+
270
+ ```python
271
+ def save(model: Model, destination: BinaryIO, *, target_version: int | None = None) -> None:
272
+ ...
273
+ ```
274
+
275
+ Avoid an ambiguous positional extension:
276
+
277
+ ```python
278
+ def save(model, destination, target_version=None):
279
+ ...
280
+ ```
281
+
282
+ ## Comments and docstrings
283
+
284
+ - Comments explain why, invariants, evidence, ownership, or a surprising constraint.
285
+ - Do not narrate syntax or preserve implementation history in comments.
286
+ - Public modules, classes, and functions must have concise docstrings.
287
+ - A docstring should state the contract, units, mutation, returned value, and expected failures when relevant.
288
+ - Link canonical documentation instead of duplicating long format tables in code comments.
289
+ - Put user guidance in `docs/`, durable contributor rules in `.agents/`, and implementation history in Git.
290
+
291
+ Preferred comment:
292
+
293
+ ```python
294
+ # Class and object entries share one index space; separate counters shift every later reference.
295
+ object_index = index_table.register_object(class_name, schema)
296
+ ```
297
+
298
+ Avoid:
299
+
300
+ ```python
301
+ # Register the object.
302
+ object_index = index_table.register_object(class_name, schema)
303
+ ```
304
+
305
+ Preferred docstring:
306
+
307
+ ```python
308
+ def read_exact(self, size: int, label: str) -> bytes:
309
+ """Read exactly *size* bytes or raise ``EOFError`` naming *label*."""
310
+ ```
311
+
312
+ ## Binary I/O
313
+
314
+ - Treat the wire format as untrusted input.
315
+ - Read and write fields in serialized order; keep version gates adjacent to the affected fields.
316
+ - Use explicit little-endian widths: `<B`, `<H`, `<I`, `<Q`, `<f`, and `<d`.
317
+ - Name offsets by what they delimit: `payload_start_offset`, `header_end_offset`, `zip_offset`.
318
+ - Check lengths before allocation or slicing, and reject unsafe container entries.
319
+ - Preserve unknown bytes only when their exact boundary is proven.
320
+ - Separate archive identity resolution from construction of public model IDs.
321
+ - Cite observed evidence in tests or format documentation; do not infer layouts from nearby versions.
322
+
323
+ Preferred:
324
+
325
+ ```python
326
+ expected = struct.pack("<I3d", 3, 1.0, 2.0, 3.0)
327
+ count, x, y, z = struct.unpack("<I3d", expected)
328
+ ```
329
+
330
+ Avoid platform-dependent widths or byte order:
331
+
332
+ ```python
333
+ value = struct.pack("I", count)
334
+ ```
335
+
336
+ Version gates should be linear and local:
337
+
338
+ ```python
339
+ name = reader.read_legacy_utf16_string("component name")
340
+ if class_version >= 5:
341
+ guid = reader.read_exact(16, "component GUID")
342
+ ```
343
+
344
+ ## Tests
345
+
346
+ - Each test should demonstrate one behavior or one tightly related boundary table.
347
+ - Test names should describe the input condition and observable result.
348
+ - Use Arrange/Act/Assert structure without adding those comments when the phases are already obvious.
349
+ - Assert public behavior and meaningful state, not incidental implementation calls.
350
+ - Cover valid boundaries, malformed lengths, truncation, unsupported schemas, and unresolved references.
351
+ - Keep fixtures small. Place a helper in a shared fixture module only when multiple test files genuinely need it.
352
+ - Use `pytest.mark.parametrize` for the same behavior across data cases.
353
+ - Do not weaken assertions merely to make a test pass.
354
+
355
+ ### Parser fixtures
356
+
357
+ Parser fixtures must be independent from production serializers, tag enums, masks, schemas, and defaults.
358
+
359
+ Preferred:
360
+
361
+ ```python
362
+ payload = b"".join(
363
+ [
364
+ struct.pack("<I", 2),
365
+ struct.pack("<3d", 1.0, 2.0, 3.0),
366
+ struct.pack("<3d", 4.0, 5.0, 6.0),
367
+ ]
368
+ )
369
+
370
+ points = read_points(io.BytesIO(payload))
371
+
372
+ assert [point.to_tuple() for point in points] == [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]
373
+ ```
374
+
375
+ Avoid constructing expected input with production code:
376
+
377
+ ```python
378
+ payload = skppy.writer.write_points(expected_points)
379
+ assert read_points(io.BytesIO(payload)) == expected_points
380
+ ```
381
+
382
+ ### Writer fixtures
383
+
384
+ Writer tests must compare generated bytes with independently authored raw expected bytes. Parser/writer round trips are
385
+ not correctness tests because the same misunderstanding can exist on both sides.
386
+
387
+ Preferred:
388
+
389
+ ```python
390
+ expected = struct.pack("<HI", 0x01FB, 4) + b"data"
391
+
392
+ actual = write_record(tag=0x01FB, payload=b"data")
393
+
394
+ assert actual == expected
395
+ ```
396
+
397
+ Avoid:
398
+
399
+ ```python
400
+ actual = write_model(model)
401
+ assert parse_model(actual) == model
402
+ ```
403
+
404
+ Round trips may exist as smoke tests only when independent byte-level tests already establish correctness.
405
+
406
+ ### Test doubles
407
+
408
+ - Prefer real small values and `io.BytesIO` over mocks.
409
+ - Use a stub or `SimpleNamespace` when the test needs a narrow protocol boundary.
410
+ - Use monkeypatching to isolate expensive/external behavior, not to reproduce the implementation under test.
411
+ - Never access the network in unit tests.
412
+
413
+ ## Blender addon code
414
+
415
+ - Keep `bpy` imports and Blender object manipulation inside `blender_skp_io`.
416
+ - Convert Blender state to/from shared `skppy` model objects at explicit adapter boundaries.
417
+ - Avoid relying on the interactive context when a data API operation is available.
418
+ - Preserve hierarchy, transforms, materials, UVs, visibility, and shared mesh identity explicitly.
419
+ - Integration behavior must be tested in every supported live Blender LTS version from the minimum supported version.
420
+
421
+ Preferred boundary:
422
+
423
+ ```python
424
+ def build_model_from_scene(scene: bpy.types.Scene) -> Model:
425
+ model = Model()
426
+ populate_model_entities(model, scene.objects)
427
+ return model
428
+ ```
429
+
430
+ Avoid importing Blender into the library:
431
+
432
+ ```python
433
+ # skppy/data_structure/model.py
434
+ import bpy
435
+ ```
436
+
437
+ ## Performance
438
+
439
+ - Choose clear linear passes and indexed lookups before micro-optimizing syntax.
440
+ - Avoid repeated full scans inside loops when an identity or ID map can be built once.
441
+ - Do not optimize without preserving readable invariants and equivalent tests.
442
+ - Benchmark changes to triangulation, large-model parsing, mesh preparation, or serialization when performance motivates
443
+ the change.
444
+
445
+ Preferred:
446
+
447
+ ```python
448
+ materials_by_name = {material.name: material for material in model.materials}
449
+ for archived in archived_materials:
450
+ material = materials_by_name.get(archived.material.name)
451
+ ```
452
+
453
+ Avoid:
454
+
455
+ ```python
456
+ for archived in archived_materials:
457
+ material = next((item for item in model.materials if item.name == archived.material.name), None)
458
+ ```
459
+
460
+ ## Required checks
461
+
462
+ Follow `.agents/TESTING.md`. At minimum, relevant changes must pass:
463
+
464
+ ```bash
465
+ make test
466
+ make coverage
467
+ make quality
468
+ ```
469
+
470
+ Run documentation, doctest, Blender, and public SDK conformance gates when the changed area requires them. A change is
471
+ not complete while any relevant formatting, lint, typing, test, coverage, documentation, or integration check fails.
472
+
473
+ ## Review checklist
474
+
475
+ Before committing, verify:
476
+
477
+ - The code is no more complex than the behavior requires.
478
+ - Names expose intent and units.
479
+ - Public and nontrivial boundaries are typed.
480
+ - Invalid input fails explicitly and with context.
481
+ - Binary fields use explicit widths and verified ordering.
482
+ - Tests are independent and cover both success and failure boundaries.
483
+ - Comments explain constraints rather than syntax.
484
+ - Lines are at most 120 characters by default, and Ruff is clean.
485
+ - The commit contains one coherent package and preserves unrelated changes.
@@ -0,0 +1,46 @@
1
+ # Agent Guide
2
+
3
+ This directory contains the small amount of project-specific context needed by
4
+ automated contributors. User-facing behavior belongs in `docs/`; implementation
5
+ history belongs in Git.
6
+
7
+ ## Project identity
8
+
9
+ - Author and maintainer: Dante Dex (`dante.dex.arch@gmail.com`)
10
+ - Library: <https://github.com/dantedex/skppy>
11
+ - Conformance suite: <https://github.com/dantedex/skppy-tests>
12
+
13
+ ## Evidence boundary
14
+
15
+ Format work must be independently reproducible. Acceptable evidence is limited
16
+ to public documentation, documented public APIs, controlled files generated by
17
+ those APIs, user-owned compatibility samples, and original black-box
18
+ experiments. Do not add proprietary source, confidential material, or assets
19
+ whose redistribution rights are unclear.
20
+
21
+ Record confirmed behavior as tests first. Describe byte layouts as observed
22
+ wire data and distinguish verified fields from unknown values. Unsupported or
23
+ uncertain layouts must fail explicitly instead of being guessed.
24
+
25
+ ## Repository map
26
+
27
+ - `skppy/`: public model, parsers, writer, and utilities.
28
+ - `blender_skp_io/`: self-contained Blender import/export extension.
29
+ - `tests/`: independent unit and integration tests.
30
+ - `docs/`: user guides, format references, API docs, and architecture.
31
+ - `.agents/CODE_STYLE.md`: mandatory compact code style and formatting rules.
32
+ - `.agents/TESTING.md`: fixture independence and required validation.
33
+
34
+ Avoid duplicating format tables or architecture notes in `.agents`. Link to the
35
+ canonical public document or source module instead.
36
+
37
+ ## Working rules
38
+
39
+ - Preserve unrelated user changes and keep commits focused.
40
+ - Use raw literal bytes in parser/writer wire tests; never make the parser and
41
+ writer validate one another.
42
+ - Keep public documentation concise. Put detailed wire tables under
43
+ `docs/format/reference/` and addon internals under `docs/blender/`.
44
+ - Never commit credentials, machine-specific paths, generated build trees, or
45
+ proprietary SDK binaries. Use only the public repository URLs listed above.
46
+ - Run the gates in `TESTING.md` before handing off a change.
@@ -0,0 +1,35 @@
1
+ # Testing Rules
2
+
3
+ ## Independent wire fixtures
4
+
5
+ - Parser fixtures use literal bytes and test-local framing helpers. They must
6
+ not import production tag enums, serializers, masks, defaults, or schemas.
7
+ - Writer unit tests compare generated bytes with independently authored raw
8
+ expected bytes. Do not use writer-to-parser round trips as correctness tests.
9
+ - Encode numeric fields with `struct.pack()` and an explicit little-endian
10
+ width such as `<H`, `<I`, `<Q`, `<f`, or `<d`.
11
+ - Assert decoded values and complete input consumption for low-level readers.
12
+ - Cover malformed lengths, truncation, missing required records, unresolved
13
+ references, unsupported schemas, and unsafe container entries.
14
+
15
+ The companion public conformance suite is
16
+ <https://github.com/dantedex/skppy-tests>. It generates fixtures through the
17
+ documented SketchUp C API and validates files written by `skppy` through that
18
+ same public API.
19
+
20
+ ## Required local gates
21
+
22
+ ```bash
23
+ make test
24
+ make coverage
25
+ make quality
26
+ make docs
27
+ make doctest
28
+ ```
29
+
30
+ `make coverage` must report 100% statement coverage for the `skppy` package.
31
+ The same threshold is enforced by GitHub Actions.
32
+
33
+ Run `make test-blender` for addon changes. Changes to serialized output also
34
+ require the matching Python-to-C validators from `skppy-tests` when the public
35
+ SDK test environment is available.
@@ -0,0 +1,116 @@
1
+ name: Blender Addon
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-addon:
14
+ name: Build Blender Addon
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v6
23
+ with:
24
+ python-version: "3.12"
25
+ cache: pip
26
+ cache-dependency-path: pyproject.toml
27
+
28
+ - name: Install build dependencies
29
+ run: pip install setuptools-scm
30
+
31
+ - name: Build addon package
32
+ run: python build_blender_addon.py
33
+
34
+ - name: Upload addon artifact
35
+ uses: actions/upload-artifact@v7
36
+ with:
37
+ name: blender-skp-io-addon
38
+ path: dist/*.zip
39
+
40
+ test-io:
41
+ name: Test Blender ${{ matrix.blender-version }} IO
42
+ needs: build-addon
43
+ runs-on: ubuntu-latest
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ include:
48
+ - blender-version: "4.5.11"
49
+ release: Blender4.5
50
+ archive: blender-4.5.11-linux-x64.tar.xz
51
+ directory: blender-4.5.11-linux-x64
52
+ - blender-version: "5.2.0"
53
+ release: Blender5.2
54
+ archive: blender-5.2.0-linux-x64.tar.xz
55
+ directory: blender-5.2.0-linux-x64
56
+ steps:
57
+ - uses: actions/checkout@v6
58
+
59
+ - name: Download addon artifact
60
+ uses: actions/download-artifact@v8
61
+ with:
62
+ name: blender-skp-io-addon
63
+ path: /tmp/addon/
64
+
65
+ - name: Cache Blender ${{ matrix.blender-version }}
66
+ id: blender-cache
67
+ uses: actions/cache@v5
68
+ with:
69
+ path: ${{ runner.temp }}/${{ matrix.directory }}
70
+ key: blender-${{ runner.os }}-${{ matrix.archive }}
71
+
72
+ - name: Download official Blender ${{ matrix.blender-version }}
73
+ if: steps.blender-cache.outputs.cache-hit != 'true'
74
+ run: |
75
+ curl --fail --location --retry 3 \
76
+ --output "${RUNNER_TEMP}/${{ matrix.archive }}" \
77
+ "https://download.blender.org/release/${{ matrix.release }}/${{ matrix.archive }}"
78
+ tar -xf "${RUNNER_TEMP}/${{ matrix.archive }}" -C "${RUNNER_TEMP}"
79
+
80
+ - name: Configure Blender executable
81
+ run: |
82
+ test -x "${RUNNER_TEMP}/${{ matrix.directory }}/blender"
83
+ echo "BLENDER_BIN=${RUNNER_TEMP}/${{ matrix.directory }}/blender" >> "${GITHUB_ENV}"
84
+
85
+ - name: Run isolated addon integration
86
+ run: |
87
+ addon_archive=$(find /tmp/addon -maxdepth 1 -name '*.zip' -print -quit)
88
+ test -n "${addon_archive}"
89
+ BLENDER_USER_RESOURCES=$(mktemp -d)
90
+ export BLENDER_USER_RESOURCES
91
+ "${BLENDER_BIN}" --background -noaudio --factory-startup \
92
+ --python-exit-code 1 \
93
+ --python tests/blender/run_integration.py -- \
94
+ --addon "${addon_archive}" \
95
+ --export-output "${RUNNER_TEMP}/blender-export/blender_export.skp"
96
+
97
+ - name: Check out skppy-tests
98
+ uses: actions/checkout@v6
99
+ with:
100
+ repository: dantedex/skppy-tests
101
+ path: skppy_tests
102
+ # Fine-grained PAT with Contents: Read when the suite is private.
103
+ token: ${{ secrets.SKPPY_TESTS_TOKEN || github.token }}
104
+ persist-credentials: false
105
+
106
+ - name: Install public SDK validator toolchain
107
+ run: |
108
+ sudo apt-get update
109
+ sudo apt-get install --yes --no-install-recommends gcc-mingw-w64-x86-64 wine
110
+
111
+ - name: Validate Blender export through the public SDK
112
+ working-directory: skppy_tests
113
+ run: >-
114
+ make blender-validation
115
+ BLENDER_EXPORT_DIR="${RUNNER_TEMP}/blender-export"
116
+ OUTPUT_DIR="${RUNNER_TEMP}/skppy-tests-blender-${{ matrix.blender-version }}"