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.
- skppy-0.9.0/.agents/CODE_STYLE.md +485 -0
- skppy-0.9.0/.agents/README.md +46 -0
- skppy-0.9.0/.agents/TESTING.md +35 -0
- skppy-0.9.0/.github/workflows/blender-addon.yml +116 -0
- skppy-0.9.0/.github/workflows/code-quality.yml +79 -0
- skppy-0.9.0/.github/workflows/docs.yml +61 -0
- skppy-0.9.0/.github/workflows/publish.yml +199 -0
- skppy-0.9.0/.github/workflows/test.yml +181 -0
- skppy-0.9.0/.gitignore +77 -0
- skppy-0.9.0/CHANGELOG.md +106 -0
- skppy-0.9.0/LICENSE +21 -0
- skppy-0.9.0/Makefile +41 -0
- skppy-0.9.0/PKG-INFO +159 -0
- skppy-0.9.0/README.md +125 -0
- skppy-0.9.0/blender_skp_io/__init__.py +58 -0
- skppy-0.9.0/blender_skp_io/annotation_builder.py +289 -0
- skppy-0.9.0/blender_skp_io/blender_manifest.toml +18 -0
- skppy-0.9.0/blender_skp_io/export_builder.py +802 -0
- skppy-0.9.0/blender_skp_io/operators/__init__.py +5 -0
- skppy-0.9.0/blender_skp_io/operators/export_skp.py +167 -0
- skppy-0.9.0/blender_skp_io/operators/import_skp.py +330 -0
- skppy-0.9.0/blender_skp_io/scene_builder.py +1143 -0
- skppy-0.9.0/build_blender_addon.py +300 -0
- skppy-0.9.0/docs/Makefile +28 -0
- skppy-0.9.0/docs/README.md +32 -0
- skppy-0.9.0/docs/_static/.gitkeep +0 -0
- skppy-0.9.0/docs/_templates/autosummary/module.rst +6 -0
- skppy-0.9.0/docs/api/index.rst +53 -0
- skppy-0.9.0/docs/blender/architecture.md +282 -0
- skppy-0.9.0/docs/blender/building.md +106 -0
- skppy-0.9.0/docs/blender/export_options.md +140 -0
- skppy-0.9.0/docs/blender/import_options.md +241 -0
- skppy-0.9.0/docs/blender/index.md +37 -0
- skppy-0.9.0/docs/blender/installing.md +81 -0
- skppy-0.9.0/docs/blender/scene_organization.md +124 -0
- skppy-0.9.0/docs/conf.py +90 -0
- skppy-0.9.0/docs/format/edge_shading.md +100 -0
- skppy-0.9.0/docs/format/index.md +30 -0
- skppy-0.9.0/docs/format/old_format.md +126 -0
- skppy-0.9.0/docs/format/reference/legacy_classes.md +122 -0
- skppy-0.9.0/docs/format/reference/legacy_container.md +140 -0
- skppy-0.9.0/docs/format/reference/legacy_fields.md +893 -0
- skppy-0.9.0/docs/format/skp_format.md +558 -0
- skppy-0.9.0/docs/format/uv_projection.md +227 -0
- skppy-0.9.0/docs/getting_started.md +195 -0
- skppy-0.9.0/docs/guides/components.md +201 -0
- skppy-0.9.0/docs/guides/errors.md +160 -0
- skppy-0.9.0/docs/guides/examples.md +65 -0
- skppy-0.9.0/docs/guides/geometry.md +177 -0
- skppy-0.9.0/docs/guides/index.md +25 -0
- skppy-0.9.0/docs/guides/materials.md +176 -0
- skppy-0.9.0/docs/guides/reading.md +186 -0
- skppy-0.9.0/docs/guides/writing.md +270 -0
- skppy-0.9.0/docs/index.rst +51 -0
- skppy-0.9.0/docs/installing.md +87 -0
- skppy-0.9.0/docs/requirements.txt +7 -0
- skppy-0.9.0/docs/skp_tags.yaml +1418 -0
- skppy-0.9.0/pyproject.toml +69 -0
- skppy-0.9.0/ruff.toml +25 -0
- skppy-0.9.0/setup.cfg +4 -0
- skppy-0.9.0/skppy/__init__.py +269 -0
- skppy-0.9.0/skppy/_cancellation.py +32 -0
- skppy-0.9.0/skppy/_version.py +24 -0
- skppy-0.9.0/skppy/coplanar.py +146 -0
- skppy-0.9.0/skppy/data_structure/__init__.py +149 -0
- skppy-0.9.0/skppy/data_structure/annotations.py +128 -0
- skppy-0.9.0/skppy/data_structure/construction.py +264 -0
- skppy-0.9.0/skppy/data_structure/document.py +143 -0
- skppy-0.9.0/skppy/data_structure/entities.py +1254 -0
- skppy-0.9.0/skppy/data_structure/header.py +80 -0
- skppy-0.9.0/skppy/data_structure/images.py +72 -0
- skppy-0.9.0/skppy/data_structure/layers.py +93 -0
- skppy-0.9.0/skppy/data_structure/materials.py +102 -0
- skppy-0.9.0/skppy/data_structure/mesh_indexing.py +125 -0
- skppy-0.9.0/skppy/data_structure/mesh_preparation.py +243 -0
- skppy-0.9.0/skppy/data_structure/meta.py +51 -0
- skppy-0.9.0/skppy/data_structure/model.py +484 -0
- skppy-0.9.0/skppy/data_structure/model_metadata.py +858 -0
- skppy-0.9.0/skppy/data_structure/primitives.py +476 -0
- skppy-0.9.0/skppy/data_structure/scene.py +371 -0
- skppy-0.9.0/skppy/data_structure/scene_data.py +112 -0
- skppy-0.9.0/skppy/data_structure/scene_graph.py +109 -0
- skppy-0.9.0/skppy/exceptions.py +100 -0
- skppy-0.9.0/skppy/legacy_writter/__init__.py +6 -0
- skppy-0.9.0/skppy/legacy_writter/envelope.py +311 -0
- skppy-0.9.0/skppy/legacy_writter/extensions.py +299 -0
- skppy-0.9.0/skppy/legacy_writter/model.py +1368 -0
- skppy-0.9.0/skppy/loader.py +187 -0
- skppy-0.9.0/skppy/parser/__init__.py +35 -0
- skppy-0.9.0/skppy/parser/attributes.py +111 -0
- skppy-0.9.0/skppy/parser/background_images.py +102 -0
- skppy-0.9.0/skppy/parser/camera_parser.py +83 -0
- skppy-0.9.0/skppy/parser/definitions.py +128 -0
- skppy-0.9.0/skppy/parser/entities.py +1049 -0
- skppy-0.9.0/skppy/parser/header_parser.py +317 -0
- skppy-0.9.0/skppy/parser/layers.py +221 -0
- skppy-0.9.0/skppy/parser/material_parser.py +545 -0
- skppy-0.9.0/skppy/parser/meta_parser.py +220 -0
- skppy-0.9.0/skppy/parser/model_parser.py +708 -0
- skppy-0.9.0/skppy/parser/rendering_options.py +175 -0
- skppy-0.9.0/skppy/parser/scenes_parser.py +181 -0
- skppy-0.9.0/skppy/parser/tlv.py +1123 -0
- skppy-0.9.0/skppy/parser/vray_materials.py +185 -0
- skppy-0.9.0/skppy/parser/zip_entries.py +65 -0
- skppy-0.9.0/skppy/parser_legacy/__init__.py +57 -0
- skppy-0.9.0/skppy/parser_legacy/annotation_payloads.py +80 -0
- skppy-0.9.0/skppy/parser_legacy/annotation_readers.py +178 -0
- skppy-0.9.0/skppy/parser_legacy/attribute_builder.py +47 -0
- skppy-0.9.0/skppy/parser_legacy/attribute_payloads.py +101 -0
- skppy-0.9.0/skppy/parser_legacy/attribute_readers.py +102 -0
- skppy-0.9.0/skppy/parser_legacy/base_payloads.py +192 -0
- skppy-0.9.0/skppy/parser_legacy/binary.py +370 -0
- skppy-0.9.0/skppy/parser_legacy/camera_payloads.py +109 -0
- skppy-0.9.0/skppy/parser_legacy/class_support.py +16 -0
- skppy-0.9.0/skppy/parser_legacy/colors.py +10 -0
- skppy-0.9.0/skppy/parser_legacy/component_body.py +109 -0
- skppy-0.9.0/skppy/parser_legacy/component_builder.py +104 -0
- skppy-0.9.0/skppy/parser_legacy/component_payloads.py +104 -0
- skppy-0.9.0/skppy/parser_legacy/component_readers.py +126 -0
- skppy-0.9.0/skppy/parser_legacy/diagnostics.py +293 -0
- skppy-0.9.0/skppy/parser_legacy/entity_builder.py +594 -0
- skppy-0.9.0/skppy/parser_legacy/envelope.py +95 -0
- skppy-0.9.0/skppy/parser_legacy/errors.py +70 -0
- skppy-0.9.0/skppy/parser_legacy/extensions.py +258 -0
- skppy-0.9.0/skppy/parser_legacy/geometry_payloads.py +200 -0
- skppy-0.9.0/skppy/parser_legacy/geometry_readers.py +582 -0
- skppy-0.9.0/skppy/parser_legacy/image_payloads.py +180 -0
- skppy-0.9.0/skppy/parser_legacy/layer_payloads.py +237 -0
- skppy-0.9.0/skppy/parser_legacy/line_style_payloads.py +47 -0
- skppy-0.9.0/skppy/parser_legacy/material_builder.py +89 -0
- skppy-0.9.0/skppy/parser_legacy/material_payloads.py +136 -0
- skppy-0.9.0/skppy/parser_legacy/metadata_payloads.py +278 -0
- skppy-0.9.0/skppy/parser_legacy/metadata_readers.py +115 -0
- skppy-0.9.0/skppy/parser_legacy/model_builder.py +229 -0
- skppy-0.9.0/skppy/parser_legacy/model_tail.py +143 -0
- skppy-0.9.0/skppy/parser_legacy/object_dispatch.py +124 -0
- skppy-0.9.0/skppy/parser_legacy/object_readers.py +350 -0
- skppy-0.9.0/skppy/parser_legacy/options_payloads.py +90 -0
- skppy-0.9.0/skppy/parser_legacy/parser.py +468 -0
- skppy-0.9.0/skppy/parser_legacy/parser_types.py +413 -0
- skppy-0.9.0/skppy/parser_legacy/provenance.py +138 -0
- skppy-0.9.0/skppy/parser_legacy/read_context.py +107 -0
- skppy-0.9.0/skppy/parser_legacy/recovery.py +448 -0
- skppy-0.9.0/skppy/parser_legacy/relationship_payloads.py +55 -0
- skppy-0.9.0/skppy/parser_legacy/rendering_options.py +177 -0
- skppy-0.9.0/skppy/parser_legacy/root_component.py +14 -0
- skppy-0.9.0/skppy/parser_legacy/root_payloads.py +23 -0
- skppy-0.9.0/skppy/parser_legacy/scene_builder.py +58 -0
- skppy-0.9.0/skppy/parser_legacy/scene_pages.py +382 -0
- skppy-0.9.0/skppy/parser_legacy/schema.py +199 -0
- skppy-0.9.0/skppy/parser_legacy/session.py +63 -0
- skppy-0.9.0/skppy/parser_legacy/uv_payloads.py +69 -0
- skppy-0.9.0/skppy/parser_legacy/visual_payloads.py +185 -0
- skppy-0.9.0/skppy/triangulation.py +1072 -0
- skppy-0.9.0/skppy/utils.py +117 -0
- skppy-0.9.0/skppy/writer/__init__.py +40 -0
- skppy-0.9.0/skppy/writer/annotation_styles.py +139 -0
- skppy-0.9.0/skppy/writer/attributes.py +89 -0
- skppy-0.9.0/skppy/writer/background_images.py +140 -0
- skppy-0.9.0/skppy/writer/cameras.py +127 -0
- skppy-0.9.0/skppy/writer/container.py +78 -0
- skppy-0.9.0/skppy/writer/definitions.py +148 -0
- skppy-0.9.0/skppy/writer/entities.py +1531 -0
- skppy-0.9.0/skppy/writer/environments.py +153 -0
- skppy-0.9.0/skppy/writer/fonts.py +56 -0
- skppy-0.9.0/skppy/writer/layers.py +229 -0
- skppy-0.9.0/skppy/writer/line_styles.py +132 -0
- skppy-0.9.0/skppy/writer/materials.py +219 -0
- skppy-0.9.0/skppy/writer/model_data.py +567 -0
- skppy-0.9.0/skppy/writer/model_metadata.py +205 -0
- skppy-0.9.0/skppy/writer/options.py +58 -0
- skppy-0.9.0/skppy/writer/scenes.py +202 -0
- skppy-0.9.0/skppy/writer/styles.py +306 -0
- skppy-0.9.0/skppy/writer/sun_data.py +19 -0
- skppy-0.9.0/skppy/writer/tlv.py +34 -0
- skppy-0.9.0/skppy/writer/vray_materials.py +292 -0
- skppy-0.9.0/skppy/writer/watermarks.py +119 -0
- skppy-0.9.0/skppy.egg-info/PKG-INFO +159 -0
- skppy-0.9.0/skppy.egg-info/SOURCES.txt +298 -0
- skppy-0.9.0/skppy.egg-info/dependency_links.txt +1 -0
- skppy-0.9.0/skppy.egg-info/requires.txt +14 -0
- skppy-0.9.0/skppy.egg-info/scm_file_list.json +294 -0
- skppy-0.9.0/skppy.egg-info/scm_version.json +8 -0
- skppy-0.9.0/skppy.egg-info/top_level.txt +1 -0
- skppy-0.9.0/tests/__init__.py +2 -0
- skppy-0.9.0/tests/blender/README.md +29 -0
- skppy-0.9.0/tests/blender/__init__.py +2 -0
- skppy-0.9.0/tests/blender/fixture_data.py +67 -0
- skppy-0.9.0/tests/blender/run_integration.py +763 -0
- skppy-0.9.0/tests/legacy_format/README.md +22 -0
- skppy-0.9.0/tests/legacy_format/__init__.py +2 -0
- skppy-0.9.0/tests/legacy_format/_fixtures.py +1076 -0
- skppy-0.9.0/tests/legacy_format/test_annotations.py +250 -0
- skppy-0.9.0/tests/legacy_format/test_archive.py +361 -0
- skppy-0.9.0/tests/legacy_format/test_auxiliary_objects.py +352 -0
- skppy-0.9.0/tests/legacy_format/test_builder_dispatch_boundaries.py +441 -0
- skppy-0.9.0/tests/legacy_format/test_diagnostic_parser_boundaries.py +125 -0
- skppy-0.9.0/tests/legacy_format/test_diagnostics.py +326 -0
- skppy-0.9.0/tests/legacy_format/test_extensions.py +325 -0
- skppy-0.9.0/tests/legacy_format/test_geometry.py +310 -0
- skppy-0.9.0/tests/legacy_format/test_geometry_adapter_boundaries.py +236 -0
- skppy-0.9.0/tests/legacy_format/test_model_assembly.py +456 -0
- skppy-0.9.0/tests/legacy_format/test_model_loading.py +443 -0
- skppy-0.9.0/tests/legacy_format/test_object_dispatch.py +928 -0
- skppy-0.9.0/tests/legacy_format/test_options_payloads.py +69 -0
- skppy-0.9.0/tests/legacy_format/test_payload_boundaries.py +272 -0
- skppy-0.9.0/tests/legacy_format/test_recovery_boundaries.py +99 -0
- skppy-0.9.0/tests/legacy_format/test_resource_payload_boundaries.py +440 -0
- skppy-0.9.0/tests/legacy_format/test_root_camera_versions.py +55 -0
- skppy-0.9.0/tests/legacy_format/test_scene_tail_boundaries.py +277 -0
- skppy-0.9.0/tests/legacy_format/test_scenes_metadata.py +491 -0
- skppy-0.9.0/tests/legacy_format/test_versioned_payloads.py +230 -0
- skppy-0.9.0/tests/legacy_writter/__init__.py +2 -0
- skppy-0.9.0/tests/legacy_writter/test_arc_raw_payload.py +39 -0
- skppy-0.9.0/tests/legacy_writter/test_attributes.py +111 -0
- skppy-0.9.0/tests/legacy_writter/test_background_images.py +71 -0
- skppy-0.9.0/tests/legacy_writter/test_cameras.py +46 -0
- skppy-0.9.0/tests/legacy_writter/test_components.py +94 -0
- skppy-0.9.0/tests/legacy_writter/test_construction.py +74 -0
- skppy-0.9.0/tests/legacy_writter/test_curves.py +142 -0
- skppy-0.9.0/tests/legacy_writter/test_definition_payloads.py +14 -0
- skppy-0.9.0/tests/legacy_writter/test_dimensions.py +130 -0
- skppy-0.9.0/tests/legacy_writter/test_envelope.py +46 -0
- skppy-0.9.0/tests/legacy_writter/test_environments.py +102 -0
- skppy-0.9.0/tests/legacy_writter/test_extensions.py +51 -0
- skppy-0.9.0/tests/legacy_writter/test_fonts.py +85 -0
- skppy-0.9.0/tests/legacy_writter/test_layer_folders.py +38 -0
- skppy-0.9.0/tests/legacy_writter/test_line_styles.py +57 -0
- skppy-0.9.0/tests/legacy_writter/test_material_pbr.py +17 -0
- skppy-0.9.0/tests/legacy_writter/test_model_metadata.py +57 -0
- skppy-0.9.0/tests/legacy_writter/test_options.py +40 -0
- skppy-0.9.0/tests/legacy_writter/test_relationships.py +49 -0
- skppy-0.9.0/tests/legacy_writter/test_rendering_options.py +40 -0
- skppy-0.9.0/tests/legacy_writter/test_resources.py +111 -0
- skppy-0.9.0/tests/legacy_writter/test_scenes.py +143 -0
- skppy-0.9.0/tests/legacy_writter/test_section_planes.py +24 -0
- skppy-0.9.0/tests/legacy_writter/test_shadow_info.py +13 -0
- skppy-0.9.0/tests/legacy_writter/test_styles.py +117 -0
- skppy-0.9.0/tests/legacy_writter/test_texts.py +80 -0
- skppy-0.9.0/tests/legacy_writter/test_uv.py +47 -0
- skppy-0.9.0/tests/legacy_writter/test_vray_materials.py +82 -0
- skppy-0.9.0/tests/test_attribute_parser.py +127 -0
- skppy-0.9.0/tests/test_background_image_parser.py +143 -0
- skppy-0.9.0/tests/test_build_blender_addon.py +104 -0
- skppy-0.9.0/tests/test_coplanar.py +76 -0
- skppy-0.9.0/tests/test_data_structure_boundaries.py +242 -0
- skppy-0.9.0/tests/test_definition_parser.py +115 -0
- skppy-0.9.0/tests/test_document.py +64 -0
- skppy-0.9.0/tests/test_entity_parser.py +224 -0
- skppy-0.9.0/tests/test_header_parser.py +107 -0
- skppy-0.9.0/tests/test_layer_parser.py +137 -0
- skppy-0.9.0/tests/test_loader.py +140 -0
- skppy-0.9.0/tests/test_material_parser.py +249 -0
- skppy-0.9.0/tests/test_meta_parser.py +89 -0
- skppy-0.9.0/tests/test_model_builders.py +187 -0
- skppy-0.9.0/tests/test_model_parser.py +65 -0
- skppy-0.9.0/tests/test_modern_entities.py +279 -0
- skppy-0.9.0/tests/test_modern_entity_boundaries.py +371 -0
- skppy-0.9.0/tests/test_modern_model_boundaries.py +305 -0
- skppy-0.9.0/tests/test_modern_model_integration.py +406 -0
- skppy-0.9.0/tests/test_modern_parser_boundaries.py +246 -0
- skppy-0.9.0/tests/test_package_version.py +34 -0
- skppy-0.9.0/tests/test_parser_fixture_independence.py +37 -0
- skppy-0.9.0/tests/test_parser_mutations.py +144 -0
- skppy-0.9.0/tests/test_prepared_mesh.py +395 -0
- skppy-0.9.0/tests/test_publication_metadata.py +45 -0
- skppy-0.9.0/tests/test_scene_parser.py +102 -0
- skppy-0.9.0/tests/test_tlv.py +83 -0
- skppy-0.9.0/tests/test_triangulation.py +147 -0
- skppy-0.9.0/tests/test_triangulation_boundaries.py +154 -0
- skppy-0.9.0/tests/test_utils.py +217 -0
- skppy-0.9.0/tests/test_uv_projection.py +216 -0
- skppy-0.9.0/tests/test_vray_materials.py +246 -0
- skppy-0.9.0/tests/test_writer_annotation_styles.py +109 -0
- skppy-0.9.0/tests/test_writer_attributes.py +267 -0
- skppy-0.9.0/tests/test_writer_background_images.py +119 -0
- skppy-0.9.0/tests/test_writer_cameras.py +96 -0
- skppy-0.9.0/tests/test_writer_container.py +96 -0
- skppy-0.9.0/tests/test_writer_definitions.py +115 -0
- skppy-0.9.0/tests/test_writer_entities.py +591 -0
- skppy-0.9.0/tests/test_writer_environments.py +152 -0
- skppy-0.9.0/tests/test_writer_images.py +76 -0
- skppy-0.9.0/tests/test_writer_layers.py +116 -0
- skppy-0.9.0/tests/test_writer_legacy.py +185 -0
- skppy-0.9.0/tests/test_writer_line_styles.py +80 -0
- skppy-0.9.0/tests/test_writer_linear_dimensions.py +115 -0
- skppy-0.9.0/tests/test_writer_materials.py +264 -0
- skppy-0.9.0/tests/test_writer_model_data.py +429 -0
- skppy-0.9.0/tests/test_writer_model_metadata.py +93 -0
- skppy-0.9.0/tests/test_writer_options.py +81 -0
- skppy-0.9.0/tests/test_writer_public_api.py +75 -0
- skppy-0.9.0/tests/test_writer_radial_dimensions.py +80 -0
- skppy-0.9.0/tests/test_writer_scenes.py +241 -0
- skppy-0.9.0/tests/test_writer_styles.py +139 -0
- skppy-0.9.0/tests/test_writer_sun_data.py +29 -0
- skppy-0.9.0/tests/test_writer_texts.py +147 -0
- skppy-0.9.0/tests/test_writer_tlv.py +57 -0
- skppy-0.9.0/tests/test_writer_vray_materials.py +174 -0
- skppy-0.9.0/tests/test_writer_watermarks.py +113 -0
- 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 }}"
|