216 lines
7.4 KiB
Python
216 lines
7.4 KiB
Python
"""Declarative required, nullable, and discriminated-union mutations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any, Iterable
|
|
|
|
from simplecadapi.scene import canonical_json_bytes, with_scene_revision
|
|
|
|
from .common import sha256_hex
|
|
|
|
|
|
Path = tuple[str | int, ...]
|
|
|
|
|
|
def _case(
|
|
name: str,
|
|
operation: str,
|
|
path: Path,
|
|
value: Any = None,
|
|
*,
|
|
recompute_revision: bool = False,
|
|
) -> dict[str, Any]:
|
|
mutation = {"operation": operation, "path": list(path)}
|
|
if operation != "delete":
|
|
mutation["value"] = value
|
|
return {
|
|
"mutations": [mutation],
|
|
"name": name,
|
|
"recompute_revision": recompute_revision,
|
|
}
|
|
|
|
|
|
def _missing_fields(path: Path, fields: Iterable[str]) -> Iterable[dict[str, Any]]:
|
|
for field in fields:
|
|
yield _case(
|
|
"missing_" + "_".join((*map(str, path), field)),
|
|
"delete",
|
|
(*path, field),
|
|
)
|
|
|
|
|
|
def scene_field_matrix(scene: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""Return compact mutations covering the frozen structural field matrix."""
|
|
|
|
records: list[tuple[Path, tuple[str, ...]]] = [
|
|
(
|
|
(),
|
|
(
|
|
"schema_version", "extensions_used", "extensions_required",
|
|
"extensions", "scene_id", "revision", "generator", "source",
|
|
"coordinate_system", "compile_options", "definitions", "nodes",
|
|
"geometry_assets", "edge_assets", "appearances", "entity_assets",
|
|
"connectors", "cameras", "lights", "annotations", "diagnostics",
|
|
),
|
|
),
|
|
(
|
|
("compile_options",),
|
|
("linear_tolerance", "angular_tolerance", "embed_source", "embed_presentation"),
|
|
),
|
|
(
|
|
("definitions", 0),
|
|
("definition_id", "kind", "name", "source", "sdk_metadata"),
|
|
),
|
|
(
|
|
("nodes", 0),
|
|
(
|
|
"node_id", "parent_node_id", "order", "definition_id", "name",
|
|
"transform", "visible", "selectable", "appearance_override_id",
|
|
"source", "sdk_metadata",
|
|
),
|
|
),
|
|
(
|
|
("appearances", 0),
|
|
(
|
|
"appearance_id", "name", "source", "base_color", "metallic",
|
|
"roughness", "alpha_mode", "double_sided", "edge_color", "sdk_metadata",
|
|
),
|
|
),
|
|
(
|
|
("geometry_assets", 0),
|
|
(
|
|
"asset_id", "uri", "media_type", "byte_length", "content_hash",
|
|
"scene_local_bounds", "asset_to_scene", "tessellation",
|
|
),
|
|
),
|
|
(
|
|
("entity_assets", 0),
|
|
("entity_asset_id", "uri", "media_type", "byte_length", "content_hash"),
|
|
),
|
|
]
|
|
result = [
|
|
mutation
|
|
for path, fields in records
|
|
for mutation in _missing_fields(path, fields)
|
|
]
|
|
result.extend(
|
|
_case(name, "add", path, True)
|
|
for name, path in (
|
|
("unknown_scene_field", ("unknown",)),
|
|
("unknown_generator_field", ("generator", "unknown")),
|
|
("unknown_compile_options_field", ("compile_options", "unknown")),
|
|
("unknown_definition_field", ("definitions", 0, "unknown")),
|
|
("unknown_node_field", ("nodes", 0, "unknown")),
|
|
("unknown_appearance_field", ("appearances", 0, "unknown")),
|
|
)
|
|
)
|
|
result.extend(
|
|
(
|
|
_case("optional_presentation_source_null", "add", ("presentation_source",), None),
|
|
_case("unknown_scene_source_kind", "set", ("source", "kind"), "unknown"),
|
|
_case(
|
|
"unknown_definition_source_kind",
|
|
"set",
|
|
("definitions", 0, "source", "kind"),
|
|
"unknown",
|
|
),
|
|
_case("missing_node_source_kind", "delete", ("nodes", 0, "source", "kind")),
|
|
_case("unsupported_scene_schema_version", "set", ("schema_version",), "2.0"),
|
|
_case(
|
|
"unsupported_generator_profile",
|
|
"set",
|
|
("generator", "profile"),
|
|
"scene-2.0",
|
|
),
|
|
_case(
|
|
"no_presentation_visibility_override",
|
|
"set",
|
|
("nodes", 0, "visible"),
|
|
False,
|
|
recompute_revision=True,
|
|
),
|
|
_case(
|
|
"no_presentation_selectability_override",
|
|
"set",
|
|
("nodes", 0, "selectable"),
|
|
False,
|
|
recompute_revision=True,
|
|
),
|
|
_case(
|
|
"no_presentation_appearance_override",
|
|
"set",
|
|
("nodes", 0, "appearance_override_id"),
|
|
scene["appearances"][0]["appearance_id"],
|
|
recompute_revision=True,
|
|
),
|
|
_case(
|
|
"no_presentation_camera",
|
|
"set",
|
|
("cameras",),
|
|
[
|
|
{
|
|
"camera_id": "camera/fixture/overview",
|
|
"far": 1000,
|
|
"name": "overview",
|
|
"near": 1,
|
|
"parent_node_id": None,
|
|
"projection": "perspective",
|
|
"transform": {
|
|
"origin": [10, -10, 10],
|
|
"x_axis": [1, 0, 0],
|
|
"y_axis": [0, 1, 0],
|
|
"z_axis": [0, 0, 1],
|
|
},
|
|
"vertical_fov_degrees": 45,
|
|
}
|
|
],
|
|
recompute_revision=True,
|
|
),
|
|
)
|
|
)
|
|
return result
|
|
|
|
|
|
def apply_scene_field_case(scene: dict[str, Any], case: dict[str, Any]) -> dict[str, Any]:
|
|
"""Replay one mutation descriptor to produce its validator input."""
|
|
|
|
result = deepcopy(scene)
|
|
for mutation in case["mutations"]:
|
|
path = mutation["path"]
|
|
current: Any = result
|
|
for part in path[:-1]:
|
|
current = current[part]
|
|
if mutation["operation"] == "delete":
|
|
del current[path[-1]]
|
|
else:
|
|
current[path[-1]] = deepcopy(mutation.get("value"))
|
|
if case.get("recompute_revision"):
|
|
result = with_scene_revision(result)
|
|
return result
|
|
|
|
|
|
def nullable_fields_nonnull_case(scene: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
"""Return the multi-field valid nullable vector and its compact descriptor."""
|
|
|
|
value = deepcopy(scene)
|
|
value["nodes"][0]["name"] = "Named occurrence"
|
|
value["appearances"][0]["name"] = "Named appearance"
|
|
appearance = value["appearances"][0]
|
|
draft = dict(appearance)
|
|
draft.pop("appearance_id")
|
|
appearance_id = "appearance/evaluated/" + sha256_hex(canonical_json_bytes(draft))
|
|
appearance["appearance_id"] = appearance_id
|
|
value["definitions"][0]["appearance_id"] = appearance_id
|
|
descriptor = {
|
|
"mutations": [
|
|
{"operation": "set", "path": ["nodes", 0, "name"], "value": "Named occurrence"},
|
|
{"operation": "set", "path": ["appearances", 0, "name"], "value": "Named appearance"},
|
|
{"operation": "set", "path": ["appearances", 0, "appearance_id"], "value": appearance_id},
|
|
{"operation": "set", "path": ["definitions", 0, "appearance_id"], "value": appearance_id},
|
|
],
|
|
"name": "nullable_fields_nonnull",
|
|
"recompute_revision": True,
|
|
}
|
|
return with_scene_revision(value), descriptor
|