Merge pull request 'N3 功能,sheet_bend 钣金折弯功能的增加,还有一些简单的代码冲突' (#15) from ganjihong into main

Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
2026-09-08 11:48:25 +08:00
7 changed files with 197 additions and 17 deletions
@@ -7,8 +7,9 @@ from typing import Any, Iterable
from build123d import Axis, Compound, Edge, Face, Location, Plane, ShapeList, Solid, Vector, Wire, export_step
from .parametric_bend import build_bend_solid
from .parametric_thread import build_thread_solid
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, ThreadSpec, TopologyRecord, Vector3, canonical_plane_signature
from .runtime_types import AxisSpec, BendSpec, HoleSpec, PlaneSpec, ThreadSpec, TopologyRecord, Vector3, canonical_plane_signature
def _vector(value: list[float] | tuple[float, float, float]) -> Vector:
@@ -410,6 +411,26 @@ class Build123dGeometryAdapter:
plane = Plane(origin=_vector(spec.axis.origin_mm), x_dir=frame_x, z_dir=direction)
return solid.moved(Location(plane))
@staticmethod
def bend_solid(spec: BendSpec) -> Any:
"""Build one bent sheet segment placed on ``spec.frame``.
The parametric generator constructs the sheet locally with the first
wing along +X, its mid-plane spanning +X/+Z (thickness along +Y) and
the fold (width) axis along +Z. This gate rotates that local frame so
the first wing lands on ``spec.frame.y_dir`` (= normal x x_dir), the
sheet thickness on ``spec.frame.normal`` and the width axis on
``spec.frame.x_dir``.
"""
solid = build_bend_solid(spec)
frame = spec.frame
plane = Plane(
origin=_vector(frame.origin_mm),
x_dir=_vector(frame.y_dir),
z_dir=_vector(frame.x_dir),
)
return solid.moved(Location(plane))
@staticmethod
def fillet(body: Any, radius_mm: float, edges: Iterable[Edge]) -> Any:
# 对指定边以给定半径做圆角。
+9 -9
View File
@@ -39,15 +39,15 @@ _BODY_MUTATING_ATOMICS = frozenset({
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
"extrude_cut_through", "loft_add",
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
"thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer",
"thread_add", "thread_cut", "bend_add", *_HOLE_ATOMICS, "fillet", "chamfer",
})
# A pattern may replay a previous pattern as well as a direct body mutation.
# Context-only features have no geometry definition to instance. thread_add
# and thread_cut are excluded: pattern translation does not yet move their
# parametric axis, so a replayed thread would silently re-run at the original
# location.
# Context-only features have no geometry definition to instance. thread_add,
# thread_cut and bend_add are excluded: pattern translation does not yet move
# their parametric axis/frame, so a replayed instance would silently re-run at
# the original location.
_REPLAYABLE_ATOMICS = (
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut"})
_BODY_MUTATING_ATOMICS - frozenset({"thread_add", "thread_cut", "bend_add"})
) | frozenset({"pattern_linear", "pattern_mirror", "pattern_circular"})
_SUPPORTED_EXTENTS = frozenset({
"blind", "mid_plane", "through_all", "through_all_both", "through_all_and_blind",
@@ -378,7 +378,7 @@ class CapabilityAnalyzer:
required.append(f"extent:{end_type}")
if end_type not in _SUPPORTED_EXTENTS:
blockers.append(self._blocker(node.feature_id, "unsupported_extent", "The extent needs a resolved topology selector or is not implemented", extent=end_type))
target_kind = _EXTENT_TARGET_KINDS.get(end_type)
target_kind = _EXTENT_TARGET_KINDS.get(end_type or "")
if target_kind:
required.append(f"selector:extent_target:{target_kind}")
reference = end_condition.get("reference")
@@ -407,7 +407,7 @@ class CapabilityAnalyzer:
node.feature_id, "unsupported_reverse_extent",
"The reverse extent is not implemented", extent=reverse_type,
))
reverse_target_kind = _EXTENT_TARGET_KINDS.get(reverse_type)
reverse_target_kind = _EXTENT_TARGET_KINDS.get(reverse_type or "")
if reverse_target_kind:
required.append(f"selector:reverse_extent_target:{reverse_target_kind}")
reverse_reference = reverse_condition.get("reference")
@@ -532,7 +532,7 @@ class CapabilityAnalyzer:
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided",
"extrude_cut_through", "loft_add",
"revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add",
"thread_add",
"thread_add", "bend_add",
# thread_cut 与 extrude_cut_blind/revolve_cut 一致:无宿主时由
# active_body 前置阻止,文档含该类特征即视为携带可执行几何。
"thread_cut",
+34 -1
View File
@@ -158,6 +158,38 @@
"required": ["radius_mm", "height_mm"],
"additionalProperties": false
},
"bendChainLeg": {
"type": "object",
"properties": {
"leg_mm": {"$ref": "#/$defs/positive"},
"bend_angle_deg": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 180},
"inner_radius_mm": {"type": "number", "minimum": 0},
"side": {"type": "integer", "enum": [1, -1]}
},
"required": ["leg_mm"],
"additionalProperties": false
},
"bendAddParams": {
"type": "object",
"properties": {
"thickness_mm": {"$ref": "#/$defs/positive"},
"width_mm": {"$ref": "#/$defs/positive"},
"chain": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/bendChainLeg"}},
"frame": {
"type": "object",
"properties": {
"origin_mm": {"$ref": "#/$defs/point3"},
"x_dir": {"$ref": "#/$defs/point3"},
"y_dir": {"$ref": "#/$defs/point3"},
"normal": {"$ref": "#/$defs/point3"}
},
"required": ["origin_mm", "x_dir", "y_dir", "normal"],
"additionalProperties": false
}
},
"required": ["thickness_mm", "width_mm", "chain"],
"additionalProperties": false
},
"threadAddParams": {
"type": "object",
"properties": {
@@ -405,7 +437,7 @@
"required": ["type", "contours"],
"additionalProperties": false
},
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "loft_add", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]},
"feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "loft_add", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "bend_add", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]},
"feature": {
"type": "object",
"properties": {
@@ -433,6 +465,7 @@
{"if": {"properties": {"atomic_id": {"const": "sphere_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/sphereParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "box_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/boxParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "cylinder_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/cylinderParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "bend_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/bendAddParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "thread_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/threadAddParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "thread_cut"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/threadCutParams"}}}},
{"if": {"properties": {"atomic_id": {"const": "hole_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/holeBlindParams"}}}},
@@ -17,6 +17,7 @@
"hole_blind": {"atomic_id":"hole_blind","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore","through_cylindrical_bore"]},
"hole_countersink": {"atomic_id":"hole_countersink","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"countersink_diameter_mm":{"type":"number","exclusiveMinimum":0},"countersink_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","countersink_diameter_mm","countersink_angle_rad"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
"hole_counterbore": {"atomic_id":"hole_counterbore","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"counterbore_diameter_mm":{"type":"number","exclusiveMinimum":0},"counterbore_depth_mm":{"type":"number","exclusiveMinimum":0},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions","counterbore_diameter_mm","counterbore_depth_mm"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore"]},
"bend_add": {"atomic_id":"bend_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"thickness_mm":{"type":"number","exclusiveMinimum":0},"width_mm":{"type":"number","exclusiveMinimum":0},"chain":{"type":"array","minItems":1,"items":{"type":"object","properties":{"leg_mm":{"type":"number","exclusiveMinimum":0},"bend_angle_deg":{"type":"number","exclusiveMinimum":0,"exclusiveMaximum":180},"inner_radius_mm":{"type":"number","minimum":0},"side":{"type":"integer","enum":[1,-1]}},"required":["leg_mm"],"additionalProperties":false}}},"required":["thickness_mm","width_mm","chain"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
"sphere_add": {"atomic_id":"sphere_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"center_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["radius_mm","center_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
"box_add": {"atomic_id":"box_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"length_mm":{"type":"number","exclusiveMinimum":0},"width_mm":{"type":"number","exclusiveMinimum":0},"height_mm":{"type":"number","exclusiveMinimum":0},"center_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["length_mm","width_mm","height_mm","center_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
"cylinder_add": {"atomic_id":"cylinder_add","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"radius_mm":{"type":"number","exclusiveMinimum":0},"height_mm":{"type":"number","exclusiveMinimum":0},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false}},"required":["radius_mm","height_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":[],"candidate_verifiers":["single_connected_body"]},
+21 -1
View File
@@ -11,7 +11,7 @@ from typing import Any, Callable, Protocol
from .build123d_adapter import Build123dGeometryAdapter
from .capabilities import CapabilityAnalyzer, pattern_transform_blocker, sketch_ids_required_by_contract
from .runtime_types import (
AxisSpec, CapabilityResult, FeaturePlanNode, FeatureResult, HoleSpec, PlaneSpec,
AxisSpec, BendSpec, CapabilityResult, FeaturePlanNode, FeatureResult, HoleSpec, PlaneSpec,
ThreadSpec, Vector3,
RuntimeDiagnostic, SelectorResolution, TopologyRecord, TopologyRegistry,
vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit,
@@ -28,6 +28,7 @@ ALL_ATOMIC_IDS = frozenset({
"hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror",
"pattern_circular",
"thread_add", "thread_cut",
"bend_add",
})
@@ -90,6 +91,7 @@ class GeometryAdapter(Protocol):
def cut(self, body: Any, tool: Any) -> Any: ...
def sphere(self, radius_mm: float, center_mm: Vector3) -> Any: ...
def thread_solid(self, spec: ThreadSpec) -> Any: ...
def bend_solid(self, spec: BendSpec) -> Any: ...
def hole_tool(self, spec: HoleSpec, starts: list[Vector3], inward: Vector3, through_depth_mm: float) -> Any: ...
def body_center(self, body: Any) -> Vector3: ...
def body_span(self, body: Any, direction: Vector3) -> float: ...
@@ -617,6 +619,23 @@ def _execute_thread_cut(node: FeaturePlanNode, session: ExecutionSession) -> Fea
return session.result(node)
def _execute_bend(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
# 折弯特征(bend_add)执行入口:按规格生成等厚折弯板并并入当前主体。
# 1. 解析并校验板厚/宽度/折痕链与放置平面,非法输入抛出带具体原因的 ValueError。
spec = BendSpec.from_feature(node.params)
# 2. 由适配器门面生成沿 spec.frame 放置的折弯实心段。
solid = session.adapter.bend_solid(spec)
# 3. 与当前主体做布尔并(fuse)后登记为新主体,并返回该特征的结果对象。
session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node)
return session.result(node)
def _bend_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
# 折弯特征(bend_add)不需要草图平面,丢弃该参数后执行。
del sketch
return _execute_bend(node, session)
def _host_plane(resolution: SelectorResolution) -> PlaneSpec:
if resolution.record is None:
raise ValueError(resolution.diagnostic.message if resolution.diagnostic else "host face was not resolved")
@@ -1419,6 +1438,7 @@ EXECUTORS: dict[str, ExecutorFunction] = {
"cylinder_add": _cylinder_executor,
"thread_add": _thread_executor,
"thread_cut": _thread_executor,
"bend_add": _bend_executor,
"extrude_add_blind": _primary_executor,
"extrude_add_two_sided": _primary_executor,
"extrude_cut_blind": _primary_executor,
+105
View File
@@ -355,6 +355,111 @@ class ThreadSpec:
)
@dataclass(frozen=True)
class BendLeg:
"""One straight wing of a bent sheet-metal chain (runtime-neutral).
``leg_mm`` is the straight mid-plane distance from the previous fold vertex
to the fold vertex leaving this wing (the unfolded chord length between the
two surrounding fold vertices). When the wing has a following wing,
``bend_angle_deg`` is the required *interior* angle between the two wings
(0 < angle < 180, 90 = a right-angle bend), ``inner_radius_mm`` the inner
bend-surface fillet radius (>= 0; the outer radius is always
``inner_radius_mm + thickness_mm``) and ``side`` the fold direction
(+1 / -1). A trailing wing carries no fold; stray fold fields on the last
wing are ignored for tolerance.
"""
leg_mm: float
bend_angle_deg: float | None = None
inner_radius_mm: float = 0.0
side: int = 1
@dataclass(frozen=True)
class BendSpec:
"""Runtime-neutral definition of a sheet-metal bend (``bend_add``).
The part is described by its mid-plane centreline: ``chain`` lists the
straight wings joined by equal-thickness bend corners. ``thickness_mm``
and ``width_mm`` are the sheet thickness and the full length along the fold
(width) axis. ``frame.origin_mm`` anchors the start of the first wing's
mid-plane path, ``frame.x_dir`` is the fold/width axis and
``frame.normal`` is the mid-plane normal of the first wing; the first wing
extends along ``normal x x_dir``.
The spec deliberately contains no OCC planes or shapes. The adapter turns
this definition into a bent solid keeping source-contract parsing separate
from B-rep work (same pattern as :class:`ThreadSpec`).
"""
thickness_mm: float
width_mm: float
frame: PlaneSpec
chain: tuple[BendLeg, ...]
@classmethod
def from_feature(cls, params: dict[str, Any]) -> "BendSpec":
try:
thickness = float(params.get("thickness_mm") or 0.0)
width = float(params.get("width_mm") or 0.0)
except (TypeError, ValueError) as error:
raise ValueError("bend thickness/width must be numeric") from error
if thickness <= 0 or width <= 0:
raise ValueError("bend requires positive thickness_mm and width_mm in millimetres")
raw_chain = params.get("chain")
if not isinstance(raw_chain, (list, tuple)) or not raw_chain:
raise ValueError("bend requires a non-empty chain of wing segments")
chain: list[BendLeg] = []
for index, raw_leg in enumerate(raw_chain):
if not isinstance(raw_leg, dict):
raise ValueError(f"bend chain[{index}] must be an object with leg_mm")
try:
leg = float(raw_leg.get("leg_mm") or 0.0)
except (TypeError, ValueError) as error:
raise ValueError(f"bend chain[{index}].leg_mm must be numeric") from error
if leg <= 0:
raise ValueError(f"bend chain[{index}].leg_mm must be positive")
bend_angle: float | None = None
if index < len(raw_chain) - 1:
raw_angle = raw_leg.get("bend_angle_deg")
if raw_angle is None:
raise ValueError(
f"bend chain[{index}] needs bend_angle_deg for the fold towards the next wing"
)
try:
bend_angle = float(raw_angle)
except (TypeError, ValueError) as error:
raise ValueError(f"bend chain[{index}].bend_angle_deg must be numeric") from error
if not 0 < bend_angle < 180:
raise ValueError("bend interior angle must be between 0 and 180 degrees")
radius = float(raw_leg.get("inner_radius_mm") or 0.0)
raw_side = raw_leg.get("side")
side = 1 if raw_side is None else int(raw_side)
if radius < 0:
raise ValueError(f"bend chain[{index}].inner_radius_mm must be non-negative")
if side not in (1, -1):
raise ValueError(f"bend chain[{index}].side must be +1 or -1")
chain.append(BendLeg(
leg_mm=leg,
bend_angle_deg=bend_angle,
inner_radius_mm=radius,
side=side,
))
raw_frame = params.get("frame")
if isinstance(raw_frame, dict):
frame = PlaneSpec.from_mapping(raw_frame)
else:
# frame 缺省:首翼沿世界 +X 延伸、厚度沿 +Y、折痕沿 +Z(贴 XY 平面)。
frame = PlaneSpec(
origin_mm=(0.0, 0.0, 0.0),
x_dir=(0.0, 0.0, 1.0),
y_dir=(1.0, 0.0, 0.0),
normal=(0.0, 1.0, 0.0),
)
return cls(thickness_mm=thickness, width_mm=width, frame=frame, chain=tuple(chain))
@dataclass(frozen=True)
class RuntimeDiagnostic:
code: str
+5 -5
View File
@@ -1,20 +1,20 @@
{
"schema": "cdsl.corpus-manifest.v1",
"corpus_version": "2026-08-31",
"corpus_version": "2026-09-07",
"document_count": 2881,
"document_stems_sha256": "ba5b905191817cd0f7b9ad07d4002ad27730e200a395f71f9c4e31ebb1867cd6",
"document_stems_sha256": "4c8660ed96f3b5427ad26adadd9b82df447951913920542501ef0fc479c02773",
"phase_pools": {
"p3": {
"selector_count": 771,
"selectors_sha256": "72093701357cd1858f2dbcc0f1ab9e6c7baea7db26357154e0586e5e1be6f801"
"selectors_sha256": "2c0f76833cd08592b71ef3a5b5b1548b649ac485e2b2388358426f815c21a047"
},
"p4": {
"selector_count": 826,
"selectors_sha256": "ab9ea6edd8532ce7a0110d0725b5303c63a53617a0af502101b12f657285d371"
"selectors_sha256": "c031552f9ae29f03ca58545d90dc8b16e2b5963f0923ff075e3724f066a2bf3d"
},
"p6": {
"selector_count": 893,
"selectors_sha256": "ac9f91b690bc533b1fdd1772d65ee77b248d108f39a429615d909b78332d9a08"
"selectors_sha256": "de44a06e55aa21940e9ccb02d0deaba90bb7669d8fb93a62bf976c903a18c42f"
}
}
}