asecli 0.6.2__py3-none-any.whl
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.
- asecli/__init__.py +3 -0
- asecli/bridge/__init__.py +40 -0
- asecli/bridge/_editor_port_contract.py +131 -0
- asecli/bridge/_editor_primitives.py +205 -0
- asecli/bridge/_editor_spec_grammar.py +245 -0
- asecli/bridge/_editor_spec_io.py +26 -0
- asecli/bridge/_editor_spec_validation.py +218 -0
- asecli/bridge/_gui_handoff_store.py +78 -0
- asecli/bridge/_gui_project.py +35 -0
- asecli/bridge/_gui_resource_store.py +159 -0
- asecli/bridge/_gui_resource_upgrade.py +122 -0
- asecli/bridge/editor_create.py +173 -0
- asecli/bridge/editor_spec.py +250 -0
- asecli/bridge/graph_geometry.py +249 -0
- asecli/bridge/graph_geometry_parser.py +57 -0
- asecli/bridge/graph_inspect.py +136 -0
- asecli/bridge/gui_handoff.py +100 -0
- asecli/bridge/gui_presentation.py +70 -0
- asecli/bridge/gui_provider_detection.py +197 -0
- asecli/bridge/gui_runtime_probe.py +25 -0
- asecli/bridge/gui_support.py +240 -0
- asecli/bridge/gui_support_resource.py +58 -0
- asecli/bridge/mcp_client.py +231 -0
- asecli/bridge/recompile.py +107 -0
- asecli/bridge/resource_text.py +12 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +238 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +235 -0
- asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +125 -0
- asecli/bridge/resources/asecli_material_gui.condition.cs.txt +99 -0
- asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +105 -0
- asecli/bridge/resources/asecli_material_gui.part00.cs.txt +300 -0
- asecli/bridge/resources/asecli_material_gui.part01.cs.txt +290 -0
- asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +174 -0
- asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +108 -0
- asecli/bridge/resources/editor_create.part00.cs.txt +176 -0
- asecli/bridge/resources/editor_create.part01.cs.txt +145 -0
- asecli/bridge/resources/editor_create.part02.cs.txt +139 -0
- asecli/bridge/resources/editor_create.part03.cs.txt +161 -0
- asecli/bridge/resources/editor_create.part04.cs.txt +114 -0
- asecli/bridge/resources/wire_route.transaction.cs.txt +140 -0
- asecli/bridge/wire_route.py +127 -0
- asecli/checks/__init__.py +10 -0
- asecli/checks/checksum.py +53 -0
- asecli/checks/local_vars.py +135 -0
- asecli/checks/usage.py +141 -0
- asecli/checks/validate.py +129 -0
- asecli/cli/__init__.py +1 -0
- asecli/cli/commands.py +237 -0
- asecli/cli/commentary_command.py +158 -0
- asecli/cli/create_command.py +250 -0
- asecli/cli/custom_gui_command.py +170 -0
- asecli/cli/gui_provider.py +30 -0
- asecli/cli/gui_support_command.py +35 -0
- asecli/cli/io.py +214 -0
- asecli/cli/layout_command.py +213 -0
- asecli/cli/main.py +245 -0
- asecli/cli/recompile_metadata.py +53 -0
- asecli/cli/skill_command.py +121 -0
- asecli/cli/usage_command.py +42 -0
- asecli/core/__init__.py +84 -0
- asecli/core/comment_bounds.py +166 -0
- asecli/core/comment_layout.py +167 -0
- asecli/core/comment_metrics.py +132 -0
- asecli/core/comment_purpose.py +140 -0
- asecli/core/commentary.py +225 -0
- asecli/core/compiled_metadata.py +135 -0
- asecli/core/compiled_properties.py +190 -0
- asecli/core/custom_gui.py +250 -0
- asecli/core/custom_gui_versions.py +35 -0
- asecli/core/fishbone_placement.py +159 -0
- asecli/core/fishbone_topology.py +186 -0
- asecli/core/graph_ops.py +122 -0
- asecli/core/layout.py +174 -0
- asecli/core/layout_audit.py +237 -0
- asecli/core/layout_audit_geometry.py +247 -0
- asecli/core/layout_audit_repeated.py +44 -0
- asecli/core/layout_graph.py +113 -0
- asecli/core/local_vars.py +54 -0
- asecli/core/material_gui_condition.py +68 -0
- asecli/core/material_gui_protocol.py +35 -0
- asecli/core/material_gui_spec.py +191 -0
- asecli/core/meticulous_layout.py +114 -0
- asecli/core/model.py +237 -0
- asecli/core/property_presentation.py +233 -0
- asecli/core/wire_geometry.py +83 -0
- asecli/core/wire_router.py +248 -0
- asecli/schema/__init__.py +49 -0
- asecli/schema/data/observed.json +16 -0
- asecli/schema/data/schemas.json +14106 -0
- asecli/skills/asecli/SKILL.md +275 -0
- asecli/skills/asecli/references/layout-standard.md +99 -0
- asecli/skills/asecli/references/master-output-settings-standard.md +51 -0
- asecli/skills/asecli/references/material-property-standard.md +108 -0
- asecli-0.6.2.dist-info/METADATA +594 -0
- asecli-0.6.2.dist-info/RECORD +98 -0
- asecli-0.6.2.dist-info/WHEEL +4 -0
- asecli-0.6.2.dist-info/entry_points.txt +2 -0
- asecli-0.6.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""ASE Register/Get Local Var semantic references."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .model import AseGraph, NodeLine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
REGISTER_LOCAL_VAR_TYPE = "AmplifyShaderEditor.RegisterLocalVarNode"
|
|
9
|
+
GET_LOCAL_VAR_TYPE = "AmplifyShaderEditor.GetLocalVarNode"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_register_local_var(node: NodeLine) -> dict:
|
|
13
|
+
if node.type_name != REGISTER_LOCAL_VAR_TYPE:
|
|
14
|
+
raise ValueError(f"node {node.node_id} is not a RegisterLocalVarNode")
|
|
15
|
+
if len(node.raw_fields) < 16:
|
|
16
|
+
raise ValueError(f"RegisterLocalVarNode {node.node_id} has too few serialized fields")
|
|
17
|
+
return {
|
|
18
|
+
"node_id": node.node_id,
|
|
19
|
+
"name": node.raw_fields[6],
|
|
20
|
+
"data_type": node.raw_fields[15],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_get_local_var(node: NodeLine) -> dict:
|
|
25
|
+
if node.type_name != GET_LOCAL_VAR_TYPE:
|
|
26
|
+
raise ValueError(f"node {node.node_id} is not a GetLocalVarNode")
|
|
27
|
+
if len(node.raw_fields) < 15:
|
|
28
|
+
raise ValueError(f"GetLocalVarNode {node.node_id} has too few serialized fields")
|
|
29
|
+
return {
|
|
30
|
+
"node_id": node.node_id,
|
|
31
|
+
"register_id": node.raw_fields[6],
|
|
32
|
+
"name": node.raw_fields[7],
|
|
33
|
+
"data_type": node.raw_fields[14],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def local_var_edges(graph: AseGraph) -> list[tuple[str, str]]:
|
|
38
|
+
"""Return valid semantic data-flow edges as ``Register -> Get``."""
|
|
39
|
+
registers = {
|
|
40
|
+
node.node_id
|
|
41
|
+
for node in graph.nodes
|
|
42
|
+
if node.type_name == REGISTER_LOCAL_VAR_TYPE
|
|
43
|
+
}
|
|
44
|
+
edges = []
|
|
45
|
+
for node in graph.nodes:
|
|
46
|
+
if node.type_name != GET_LOCAL_VAR_TYPE:
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
item = parse_get_local_var(node)
|
|
50
|
+
except ValueError:
|
|
51
|
+
continue
|
|
52
|
+
if item["register_id"] in registers:
|
|
53
|
+
edges.append((item["register_id"], node.node_id))
|
|
54
|
+
return edges
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Portable conditional-enable metadata for MZGUI-compatible inspectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ENABLE_IF_ATTRIBUTE = "EnableIfMzgui"
|
|
10
|
+
ENABLE_IF_OPERATORS = (
|
|
11
|
+
"Less",
|
|
12
|
+
"LessEqual",
|
|
13
|
+
"Equal",
|
|
14
|
+
"NotEqual",
|
|
15
|
+
"GreaterEqual",
|
|
16
|
+
"Greater",
|
|
17
|
+
)
|
|
18
|
+
_PROPERTY_NAME = re.compile(r"^_[A-Za-z][A-Za-z0-9_]{0,127}$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def enable_if_attribute(property_name: str, operator: str, value: int | float) -> str:
|
|
22
|
+
"""Encode one conditional-enable rule as a ShaderLab property attribute."""
|
|
23
|
+
condition = validate_enable_if(
|
|
24
|
+
{"property": property_name, "operator": operator, "value": value}
|
|
25
|
+
)
|
|
26
|
+
number = format(condition["value"], ".9g")
|
|
27
|
+
return (
|
|
28
|
+
f'[{ENABLE_IF_ATTRIBUTE}({condition["property"]},'
|
|
29
|
+
f'{condition["operator"]},{number})]'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_enable_if_arguments(arguments: str) -> dict:
|
|
34
|
+
"""Decode the comma-separated payload stored by EnableIfMzgui."""
|
|
35
|
+
parts = [part.strip() for part in arguments.split(",")]
|
|
36
|
+
if len(parts) != 3:
|
|
37
|
+
raise ValueError("EnableIfMzgui requires property, operator, and numeric value")
|
|
38
|
+
try:
|
|
39
|
+
value = float(parts[2])
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
raise ValueError("EnableIfMzgui value must be a finite number") from exc
|
|
42
|
+
return validate_enable_if(
|
|
43
|
+
{"property": parts[0], "operator": parts[1], "value": value}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_enable_if(value: object) -> dict:
|
|
48
|
+
"""Validate a JSON-compatible conditional-enable object."""
|
|
49
|
+
if not isinstance(value, dict):
|
|
50
|
+
raise ValueError("enabled_if must be an object")
|
|
51
|
+
unknown = set(value) - {"property", "operator", "value"}
|
|
52
|
+
if unknown:
|
|
53
|
+
raise ValueError(f"enabled_if has unknown keys: {sorted(unknown)}")
|
|
54
|
+
property_name = value.get("property")
|
|
55
|
+
if not isinstance(property_name, str) or not _PROPERTY_NAME.fullmatch(property_name):
|
|
56
|
+
raise ValueError("enabled_if.property must be an underscored Shader property name")
|
|
57
|
+
operator = value.get("operator", "Equal")
|
|
58
|
+
if operator not in ENABLE_IF_OPERATORS:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"enabled_if.operator must be one of " + ", ".join(ENABLE_IF_OPERATORS)
|
|
61
|
+
)
|
|
62
|
+
number = value.get("value")
|
|
63
|
+
if isinstance(number, bool) or not isinstance(number, (int, float)):
|
|
64
|
+
raise ValueError("enabled_if.value must be a finite number")
|
|
65
|
+
number = float(number)
|
|
66
|
+
if not math.isfinite(number):
|
|
67
|
+
raise ValueError("enabled_if.value must be a finite number")
|
|
68
|
+
return {"property": property_name, "operator": operator, "value": number}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Shared MZGUI metadata protocol and legacy migration aliases."""
|
|
2
|
+
|
|
3
|
+
PROPERTY_METADATA_ATTRIBUTE_TYPES = (
|
|
4
|
+
"FoldoutMzgui",
|
|
5
|
+
"TooltipMzgui",
|
|
6
|
+
"HelpBoxMzgui",
|
|
7
|
+
"EnableIfMzgui",
|
|
8
|
+
)
|
|
9
|
+
LEGACY_PROPERTY_METADATA_ATTRIBUTE_TYPES = (
|
|
10
|
+
"ASECLIFoldout",
|
|
11
|
+
"ASECLITooltip",
|
|
12
|
+
"ASECLIHelpBox",
|
|
13
|
+
"ASECLIEnableIf",
|
|
14
|
+
)
|
|
15
|
+
_LEGACY_BY_CANONICAL = dict(
|
|
16
|
+
zip(PROPERTY_METADATA_ATTRIBUTE_TYPES, LEGACY_PROPERTY_METADATA_ATTRIBUTE_TYPES)
|
|
17
|
+
)
|
|
18
|
+
_CANONICAL_BY_ATTRIBUTE = {
|
|
19
|
+
**{name: name for name in PROPERTY_METADATA_ATTRIBUTE_TYPES},
|
|
20
|
+
**{legacy: canonical for canonical, legacy in _LEGACY_BY_CANONICAL.items()},
|
|
21
|
+
}
|
|
22
|
+
MANAGED_PROPERTY_METADATA_ATTRIBUTE_TYPES = frozenset(_CANONICAL_BY_ATTRIBUTE)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def canonical_property_metadata_type(type_name: str) -> str | None:
|
|
26
|
+
"""Return the public MZGUI type for a public or historical ASECLI type."""
|
|
27
|
+
return _CANONICAL_BY_ATTRIBUTE.get(type_name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def equivalent_property_metadata_types(type_name: str) -> frozenset[str]:
|
|
31
|
+
"""Return the MZGUI type and its one legacy ASECLI alias."""
|
|
32
|
+
canonical = canonical_property_metadata_type(type_name)
|
|
33
|
+
if canonical is None:
|
|
34
|
+
return frozenset()
|
|
35
|
+
return frozenset((canonical, _LEGACY_BY_CANONICAL[canonical]))
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Declarative, atomic material-property ordering and ASECLI metadata specs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .custom_gui import (
|
|
6
|
+
SUPPORTED_GUI_EDITORS,
|
|
7
|
+
graph_custom_editor,
|
|
8
|
+
is_material_property_node,
|
|
9
|
+
remove_property_metadata_attribute,
|
|
10
|
+
semantic_attribute,
|
|
11
|
+
set_custom_editor,
|
|
12
|
+
set_property_metadata_attribute,
|
|
13
|
+
)
|
|
14
|
+
from .custom_gui_versions import require_property_metadata_tail_version
|
|
15
|
+
from .material_gui_condition import enable_if_attribute, validate_enable_if
|
|
16
|
+
from .model import AseFile, AseGraph, NodeLine
|
|
17
|
+
from .property_presentation import set_property_display_name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_TOP_KEYS = {"editor", "reorder", "properties"}
|
|
21
|
+
_PROPERTY_KEYS = {
|
|
22
|
+
"name", "node", "display_name", "group", "help", "tooltip", "enabled_if"
|
|
23
|
+
}
|
|
24
|
+
_SEMANTIC_TYPES = {
|
|
25
|
+
"group": "FoldoutMzgui",
|
|
26
|
+
"help": "HelpBoxMzgui",
|
|
27
|
+
"tooltip": "TooltipMzgui",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def material_property_nodes(graph: AseGraph) -> list[NodeLine]:
|
|
32
|
+
return [node for node in graph.nodes if is_material_property_node(node)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_property_node(
|
|
36
|
+
graph: AseGraph,
|
|
37
|
+
*,
|
|
38
|
+
node_id: str | None = None,
|
|
39
|
+
property_name: str | None = None,
|
|
40
|
+
) -> NodeLine:
|
|
41
|
+
"""Resolve an exported property by node id or exact ShaderLab property name."""
|
|
42
|
+
if (node_id is None) == (property_name is None):
|
|
43
|
+
raise ValueError("provide exactly one of node id or property name")
|
|
44
|
+
if node_id is not None:
|
|
45
|
+
node = graph.node_by_id(node_id)
|
|
46
|
+
if node is None:
|
|
47
|
+
raise KeyError(f"node {node_id} not found")
|
|
48
|
+
if not is_material_property_node(node):
|
|
49
|
+
raise ValueError(f"node {node_id} is not an exported PropertyNode")
|
|
50
|
+
return node
|
|
51
|
+
matches = [node for node in material_property_nodes(graph) if node.raw_fields[7] == property_name]
|
|
52
|
+
if len(matches) != 1:
|
|
53
|
+
raise ValueError(f"expected one exported property named {property_name!r}, found {len(matches)}")
|
|
54
|
+
return matches[0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply_material_gui_spec(ase_file: AseFile, spec: dict) -> list[dict]:
|
|
58
|
+
"""Apply a declarative property order/group/tooltip/optional-help spec in memory."""
|
|
59
|
+
if not isinstance(spec, dict):
|
|
60
|
+
raise ValueError("material GUI spec root must be a JSON object")
|
|
61
|
+
unknown = set(spec) - _TOP_KEYS
|
|
62
|
+
if unknown:
|
|
63
|
+
raise ValueError(f"unknown material GUI spec keys: {sorted(unknown)}")
|
|
64
|
+
entries = spec.get("properties", [])
|
|
65
|
+
if not isinstance(entries, list):
|
|
66
|
+
raise ValueError("material GUI spec 'properties' must be an array")
|
|
67
|
+
reorder = spec.get("reorder", False)
|
|
68
|
+
if not isinstance(reorder, bool):
|
|
69
|
+
raise ValueError("material GUI spec 'reorder' must be boolean")
|
|
70
|
+
|
|
71
|
+
changes: list[dict] = []
|
|
72
|
+
editor = spec.get("editor")
|
|
73
|
+
if editor is not None:
|
|
74
|
+
if not isinstance(editor, str):
|
|
75
|
+
raise ValueError("material GUI spec 'editor' must be a class-name string")
|
|
76
|
+
changes.append(set_custom_editor(ase_file, editor))
|
|
77
|
+
|
|
78
|
+
resolved: list[tuple[NodeLine, dict]] = []
|
|
79
|
+
seen: set[str] = set()
|
|
80
|
+
has_additions = False
|
|
81
|
+
for index, entry in enumerate(entries):
|
|
82
|
+
if not isinstance(entry, dict):
|
|
83
|
+
raise ValueError(f"properties[{index}] must be an object")
|
|
84
|
+
unknown = set(entry) - _PROPERTY_KEYS
|
|
85
|
+
if unknown:
|
|
86
|
+
raise ValueError(f"properties[{index}] has unknown keys: {sorted(unknown)}")
|
|
87
|
+
has_name = "name" in entry
|
|
88
|
+
has_node = "node" in entry
|
|
89
|
+
if has_name == has_node:
|
|
90
|
+
raise ValueError(f"properties[{index}] must provide exactly one of 'name' or 'node'")
|
|
91
|
+
if has_name and not isinstance(entry["name"], str):
|
|
92
|
+
raise ValueError(f"properties[{index}] field 'name' must be a string")
|
|
93
|
+
if has_node and not isinstance(entry["node"], (str, int)):
|
|
94
|
+
raise ValueError(f"properties[{index}] field 'node' must be a string or integer")
|
|
95
|
+
node = resolve_property_node(
|
|
96
|
+
ase_file.graph,
|
|
97
|
+
node_id=str(entry["node"]) if has_node else None,
|
|
98
|
+
property_name=entry.get("name"),
|
|
99
|
+
)
|
|
100
|
+
if node.node_id in seen:
|
|
101
|
+
raise ValueError(f"property node {node.node_id} appears more than once in spec")
|
|
102
|
+
seen.add(node.node_id)
|
|
103
|
+
resolved.append((node, entry))
|
|
104
|
+
has_additions |= any(entry.get(key) is not None for key in _SEMANTIC_TYPES if key in entry)
|
|
105
|
+
has_additions |= entry.get("enabled_if") is not None
|
|
106
|
+
|
|
107
|
+
active_editor = graph_custom_editor(ase_file.graph)
|
|
108
|
+
if has_additions and active_editor not in SUPPORTED_GUI_EDITORS:
|
|
109
|
+
supported = ", ".join(sorted(SUPPORTED_GUI_EDITORS))
|
|
110
|
+
raise ValueError(f"MZGUI-compatible property metadata requires a selected material GUI: {supported}")
|
|
111
|
+
if reorder and not resolved:
|
|
112
|
+
raise ValueError("reorder=true requires at least one property entry")
|
|
113
|
+
if reorder:
|
|
114
|
+
require_property_metadata_tail_version(ase_file.graph)
|
|
115
|
+
changes.extend(_reorder_properties(ase_file.graph, [node for node, _ in resolved]))
|
|
116
|
+
|
|
117
|
+
for node, entry in resolved:
|
|
118
|
+
if "display_name" in entry:
|
|
119
|
+
display_name = entry["display_name"]
|
|
120
|
+
if not isinstance(display_name, str):
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"property {node.raw_fields[7]} field 'display_name' must be a string"
|
|
123
|
+
)
|
|
124
|
+
changes.append(set_property_display_name(ase_file, node.node_id, display_name))
|
|
125
|
+
for key, type_name in _SEMANTIC_TYPES.items():
|
|
126
|
+
if key not in entry:
|
|
127
|
+
continue
|
|
128
|
+
value = entry[key]
|
|
129
|
+
if value is None:
|
|
130
|
+
changes.append(remove_property_metadata_attribute(ase_file.graph, node.node_id, type_name))
|
|
131
|
+
elif isinstance(value, str):
|
|
132
|
+
changes.append(
|
|
133
|
+
set_property_metadata_attribute(ase_file.graph, node.node_id, semantic_attribute(type_name, value))
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(f"property {node.raw_fields[7]} field {key!r} must be string or null")
|
|
137
|
+
if "enabled_if" in entry:
|
|
138
|
+
condition = entry["enabled_if"]
|
|
139
|
+
if condition is None:
|
|
140
|
+
changes.append(
|
|
141
|
+
remove_property_metadata_attribute(
|
|
142
|
+
ase_file.graph, node.node_id, "EnableIfMzgui"
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
validated = validate_enable_if(condition)
|
|
147
|
+
changes.append(
|
|
148
|
+
set_property_metadata_attribute(
|
|
149
|
+
ase_file.graph,
|
|
150
|
+
node.node_id,
|
|
151
|
+
enable_if_attribute(
|
|
152
|
+
validated["property"],
|
|
153
|
+
validated["operator"],
|
|
154
|
+
validated["value"],
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return changes
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _reorder_properties(graph: AseGraph, requested: list[NodeLine]) -> list[dict]:
|
|
162
|
+
all_nodes = material_property_nodes(graph)
|
|
163
|
+
requested_ids = {node.node_id for node in requested}
|
|
164
|
+
instruction_order = {node.node_id: index for index, node in enumerate(all_nodes)}
|
|
165
|
+
|
|
166
|
+
def current_order(node: NodeLine) -> tuple[int, int]:
|
|
167
|
+
try:
|
|
168
|
+
value = int(node.raw_fields[9])
|
|
169
|
+
except (IndexError, ValueError):
|
|
170
|
+
value = 2**31 - 1
|
|
171
|
+
return value, instruction_order[node.node_id]
|
|
172
|
+
|
|
173
|
+
remaining = sorted((node for node in all_nodes if node.node_id not in requested_ids), key=current_order)
|
|
174
|
+
changes = []
|
|
175
|
+
for order, node in enumerate([*requested, *remaining]):
|
|
176
|
+
before = node.raw_fields[9]
|
|
177
|
+
after = str(order)
|
|
178
|
+
if before == after:
|
|
179
|
+
continue
|
|
180
|
+
node.raw_fields[9] = after
|
|
181
|
+
graph.replace_node(node)
|
|
182
|
+
changes.append(
|
|
183
|
+
{
|
|
184
|
+
"kind": "property_order",
|
|
185
|
+
"node_id": node.node_id,
|
|
186
|
+
"property_name": node.raw_fields[7],
|
|
187
|
+
"before": before,
|
|
188
|
+
"after": after,
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
return changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Public recursive fishbone layout plan and application API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Mapping, Protocol
|
|
7
|
+
|
|
8
|
+
from .commentary import inspect_comment_groups
|
|
9
|
+
from .fishbone_placement import place_fishbone
|
|
10
|
+
from .fishbone_topology import build_fishbone_topology
|
|
11
|
+
from .layout import apply_positions
|
|
12
|
+
from .layout_graph import id_key, node_rect
|
|
13
|
+
from .model import AseGraph
|
|
14
|
+
from .wire_router import WIRE_NODE_TYPE, plan_wire_routes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Geometry(Protocol):
|
|
18
|
+
width: float
|
|
19
|
+
height: float
|
|
20
|
+
title_height: float
|
|
21
|
+
input_ports: dict[str, tuple[float, float]]
|
|
22
|
+
output_ports: dict[str, tuple[float, float]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class MeticulousLayoutPlan:
|
|
27
|
+
positions: dict[str, tuple[float, float]]
|
|
28
|
+
primary_parent: dict[str, str]
|
|
29
|
+
children: dict[str, tuple[str, ...]]
|
|
30
|
+
ordered_children: dict[str, tuple[str, ...]]
|
|
31
|
+
spine_child: dict[str, str]
|
|
32
|
+
roots: tuple[str, ...]
|
|
33
|
+
depths: dict[str, int]
|
|
34
|
+
stage_output_x: dict[str, float]
|
|
35
|
+
subtree_bounds: dict[str, tuple[float, float, float, float]]
|
|
36
|
+
horizontal_edges: tuple[tuple[str, str, str, str], ...]
|
|
37
|
+
secondary_edges: tuple[tuple[str, str, str, str], ...]
|
|
38
|
+
comment_snapshot: tuple[tuple[str, tuple[str, ...], str], ...]
|
|
39
|
+
wire_route_changes: tuple[dict, ...] = ()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def meticulous_layout_positions(
|
|
43
|
+
graph: AseGraph,
|
|
44
|
+
geometry: Mapping[str, Geometry],
|
|
45
|
+
*,
|
|
46
|
+
route_wires: bool = False,
|
|
47
|
+
) -> MeticulousLayoutPlan:
|
|
48
|
+
topology = build_fishbone_topology(graph, geometry)
|
|
49
|
+
positions, roots, stage_output_x = place_fishbone(graph, geometry, topology)
|
|
50
|
+
subtree_bounds = {}
|
|
51
|
+
for node_id in sorted(topology.ids, key=id_key):
|
|
52
|
+
members = _descendants(node_id, topology.children)
|
|
53
|
+
rects = [
|
|
54
|
+
node_rect(member, positions, geometry)
|
|
55
|
+
for member in members if member in positions and member in geometry
|
|
56
|
+
]
|
|
57
|
+
subtree_bounds[node_id] = _union_bounds(rects)
|
|
58
|
+
horizontal_edges = tuple(sorted(
|
|
59
|
+
(
|
|
60
|
+
child, topology.primary_wire[(child, parent)].out_port,
|
|
61
|
+
parent, topology.primary_wire[(child, parent)].in_port,
|
|
62
|
+
)
|
|
63
|
+
for parent, child in topology.spine_child.items()
|
|
64
|
+
if topology.group_of.get(parent) == topology.group_of.get(child)
|
|
65
|
+
))
|
|
66
|
+
primary_keys = set(topology.primary_parent.items())
|
|
67
|
+
secondary = tuple(sorted(
|
|
68
|
+
(wire.out_node, wire.out_port, wire.in_node, wire.in_port)
|
|
69
|
+
for wire in topology.collapsed_wires
|
|
70
|
+
if wire.out_node in topology.ids and wire.in_node in topology.ids
|
|
71
|
+
and (wire.out_node, wire.in_node) not in primary_keys
|
|
72
|
+
))
|
|
73
|
+
comments = tuple(sorted(
|
|
74
|
+
(group["node_id"], tuple(group["members"]), group["color"])
|
|
75
|
+
for group in inspect_comment_groups(graph)
|
|
76
|
+
))
|
|
77
|
+
route_changes = plan_wire_routes(graph, geometry, positions) if route_wires else ()
|
|
78
|
+
return MeticulousLayoutPlan(
|
|
79
|
+
positions=positions,
|
|
80
|
+
primary_parent=topology.primary_parent,
|
|
81
|
+
children=topology.children,
|
|
82
|
+
ordered_children=topology.children,
|
|
83
|
+
spine_child=topology.spine_child,
|
|
84
|
+
roots=tuple(roots),
|
|
85
|
+
depths=topology.depths,
|
|
86
|
+
stage_output_x=stage_output_x,
|
|
87
|
+
subtree_bounds=subtree_bounds,
|
|
88
|
+
horizontal_edges=horizontal_edges,
|
|
89
|
+
secondary_edges=secondary,
|
|
90
|
+
comment_snapshot=comments,
|
|
91
|
+
wire_route_changes=route_changes,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def apply_meticulous_layout(graph: AseGraph, plan: MeticulousLayoutPlan) -> int:
|
|
96
|
+
return apply_positions(graph, plan.positions)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _union_bounds(rects):
|
|
100
|
+
values = list(rects)
|
|
101
|
+
return (
|
|
102
|
+
min(rect[0] for rect in values), min(rect[1] for rect in values),
|
|
103
|
+
max(rect[2] for rect in values), max(rect[3] for rect in values),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _descendants(node_id: str, children: Mapping[str, tuple[str, ...]]) -> set[str]:
|
|
108
|
+
result, stack = {node_id}, list(children.get(node_id, ()))
|
|
109
|
+
while stack:
|
|
110
|
+
child = stack.pop()
|
|
111
|
+
if child not in result:
|
|
112
|
+
result.add(child)
|
|
113
|
+
stack.extend(children.get(child, ()))
|
|
114
|
+
return result
|