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,250 @@
|
|
|
1
|
+
"""Safe shader creation and semantic Shader-name mutation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from ..bridge import (
|
|
10
|
+
MZGUI_EDITOR,
|
|
11
|
+
McpError,
|
|
12
|
+
SpecError,
|
|
13
|
+
create_shader_via_mcp,
|
|
14
|
+
install_gui_support,
|
|
15
|
+
load_editor_graph_spec,
|
|
16
|
+
route_create_backend,
|
|
17
|
+
)
|
|
18
|
+
from ..checks import ChecksumFormatError, fix_checksum, validate_file
|
|
19
|
+
from ..core import (
|
|
20
|
+
AseFile,
|
|
21
|
+
apply_material_gui_spec,
|
|
22
|
+
main_master_node,
|
|
23
|
+
require_property_presentation,
|
|
24
|
+
sync_compiled_property_metadata,
|
|
25
|
+
)
|
|
26
|
+
from .commands import CliError, _commit_text, _load
|
|
27
|
+
from .gui_provider import select_editor_provider
|
|
28
|
+
from .io import UnsafeWritePathError, file_digest
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_SHADER_DECLARATION = re.compile(r'(?m)^(?P<indent>[ \t]*)Shader[ \t]+"(?P<name>[^"\r\n]+)"')
|
|
32
|
+
_TEMPLATE_NAME_FIELD = {
|
|
33
|
+
"AmplifyShaderEditor.TemplateMultiPassMasterNode": 12,
|
|
34
|
+
"AmplifyShaderEditor.TemplateMasterNode": 12,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_create(args) -> dict:
|
|
39
|
+
spec = None
|
|
40
|
+
if args.spec:
|
|
41
|
+
try:
|
|
42
|
+
spec = load_editor_graph_spec(args.spec)
|
|
43
|
+
except FileNotFoundError as exc:
|
|
44
|
+
raise CliError("NOT_FOUND", str(exc)) from exc
|
|
45
|
+
except SpecError as exc:
|
|
46
|
+
raise CliError("USAGE_ERROR", str(exc)) from exc
|
|
47
|
+
try:
|
|
48
|
+
backend = route_create_backend(args.backend, spec)
|
|
49
|
+
except SpecError as exc:
|
|
50
|
+
raise CliError("USAGE_ERROR", str(exc)) from exc
|
|
51
|
+
if backend == "editor":
|
|
52
|
+
return _cmd_create_editor(args, spec)
|
|
53
|
+
return _cmd_create_text(args, spec)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cmd_create_text(args, spec) -> dict:
|
|
57
|
+
if spec is not None:
|
|
58
|
+
raise CliError("USAGE_ERROR", "--spec requires --backend editor or auto")
|
|
59
|
+
if not args.from_template:
|
|
60
|
+
raise CliError("USAGE_ERROR", "--from is required for the text create backend")
|
|
61
|
+
f = _load(args.from_template)
|
|
62
|
+
if args.graph_from:
|
|
63
|
+
donor = _load(args.graph_from)
|
|
64
|
+
f.replace_graph(donor.graph)
|
|
65
|
+
if args.name:
|
|
66
|
+
_set_shader_name(f, args.name)
|
|
67
|
+
try:
|
|
68
|
+
text = fix_checksum(f.serialize())
|
|
69
|
+
except ChecksumFormatError as exc:
|
|
70
|
+
raise CliError("CHECKSUM_FORMAT_ERROR", str(exc)) from exc
|
|
71
|
+
try:
|
|
72
|
+
created = AseFile.from_text(text)
|
|
73
|
+
errors = [issue for issue in validate_file(created) if issue["severity"] == "error"]
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise CliError("VALIDATION_ERROR", f"created shader is not parseable: {exc}") from exc
|
|
76
|
+
if errors:
|
|
77
|
+
raise CliError(
|
|
78
|
+
"VALIDATION_ERROR",
|
|
79
|
+
f"created shader contains {len(errors)} structural error(s)",
|
|
80
|
+
{"issues": errors, "error_count": len(errors)},
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
presentation = require_property_presentation(created)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise CliError("PROPERTY_PRESENTATION_ERROR", str(exc)) from exc
|
|
86
|
+
try:
|
|
87
|
+
output_digest = file_digest(args.out)
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
output_digest = None
|
|
90
|
+
except UnsafeWritePathError as exc:
|
|
91
|
+
raise CliError("UNSAFE_PATH", str(exc)) from exc
|
|
92
|
+
if output_digest is not None and not args.force:
|
|
93
|
+
raise CliError("USAGE_ERROR", f"{args.out} exists (use --force)")
|
|
94
|
+
_commit_text(args.out, text, output_digest)
|
|
95
|
+
return {
|
|
96
|
+
"created": args.out,
|
|
97
|
+
"name": args.name,
|
|
98
|
+
"graph_from": args.graph_from,
|
|
99
|
+
"backend": "text",
|
|
100
|
+
"property_presentation": presentation,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _cmd_create_editor(args, spec) -> dict:
|
|
105
|
+
if spec is None:
|
|
106
|
+
raise CliError("USAGE_ERROR", "--spec is required for the editor create backend")
|
|
107
|
+
if spec.version not in {2, 3}:
|
|
108
|
+
raise CliError(
|
|
109
|
+
"USAGE_ERROR",
|
|
110
|
+
"CLI Editor creation requires EditorGraphSpec v2 or v3 with Chinese inspector_name and help for every property",
|
|
111
|
+
)
|
|
112
|
+
if args.from_template or args.graph_from or args.name:
|
|
113
|
+
raise CliError("USAGE_ERROR", "--from, --graph-from and --name are text-backend options")
|
|
114
|
+
if args.force:
|
|
115
|
+
raise CliError("USAGE_ERROR", "editor create only supports absent targets; --force is not allowed")
|
|
116
|
+
if args.instance_token_argv is not None:
|
|
117
|
+
raise CliError("USAGE_ERROR", "do not pass MCP tokens via argv; use ASECLI_MCP_INSTANCE_TOKEN")
|
|
118
|
+
try:
|
|
119
|
+
selected_editor, gui_support = select_editor_provider(args.out)
|
|
120
|
+
except (FileNotFoundError, ValueError, RuntimeError, OSError) as exc:
|
|
121
|
+
raise CliError("GUI_SUPPORT_ERROR", str(exc)) from exc
|
|
122
|
+
try:
|
|
123
|
+
result = create_shader_via_mcp(
|
|
124
|
+
args.out,
|
|
125
|
+
spec,
|
|
126
|
+
mcp_url=args.mcp_url,
|
|
127
|
+
instance_token=os.environ.get("ASECLI_MCP_INSTANCE_TOKEN"),
|
|
128
|
+
allow_remote_mcp=args.allow_remote_mcp,
|
|
129
|
+
)
|
|
130
|
+
except McpError as exc:
|
|
131
|
+
raise CliError("BRIDGE_ERROR", str(exc), exc.data) from exc
|
|
132
|
+
except FileExistsError as exc:
|
|
133
|
+
raise CliError("USAGE_ERROR", str(exc)) from exc
|
|
134
|
+
except FileNotFoundError as exc:
|
|
135
|
+
raise CliError("NOT_FOUND", str(exc)) from exc
|
|
136
|
+
except ValueError as exc:
|
|
137
|
+
raise CliError("USAGE_ERROR", str(exc)) from exc
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
created = AseFile.from_path(args.out)
|
|
141
|
+
errors = [issue for issue in validate_file(created) if issue["severity"] == "error"]
|
|
142
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
143
|
+
raise CliError(
|
|
144
|
+
"BRIDGE_ERROR",
|
|
145
|
+
f"Editor-created shader is not parseable and was preserved for diagnosis: {exc}",
|
|
146
|
+
_preserved_editor_asset_details(args.out, result),
|
|
147
|
+
) from exc
|
|
148
|
+
if errors:
|
|
149
|
+
raise CliError(
|
|
150
|
+
"BRIDGE_ERROR",
|
|
151
|
+
f"Editor-created shader contains {len(errors)} structural error(s) and was preserved for diagnosis",
|
|
152
|
+
{
|
|
153
|
+
"issues": errors,
|
|
154
|
+
"error_count": len(errors),
|
|
155
|
+
**_preserved_editor_asset_details(args.out, result),
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
output, presentation = _finalize_editor_property_presentation(
|
|
160
|
+
created, spec, editor=selected_editor
|
|
161
|
+
)
|
|
162
|
+
_commit_text(args.out, output, created.source_digest)
|
|
163
|
+
result["shader_sha256"] = file_digest(args.out)
|
|
164
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
165
|
+
raise CliError(
|
|
166
|
+
"BRIDGE_ERROR",
|
|
167
|
+
f"Editor-created shader failed property-presentation finalization and was preserved for diagnosis: {exc}",
|
|
168
|
+
_preserved_editor_asset_details(args.out, result),
|
|
169
|
+
) from exc
|
|
170
|
+
return {
|
|
171
|
+
"created": args.out,
|
|
172
|
+
"backend": "editor",
|
|
173
|
+
"gui_support": gui_support,
|
|
174
|
+
"property_presentation": presentation,
|
|
175
|
+
**result,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _finalize_editor_property_presentation(
|
|
180
|
+
created: AseFile, spec, *, editor: str = MZGUI_EDITOR
|
|
181
|
+
) -> tuple[str, dict]:
|
|
182
|
+
"""Apply the same v2 presentation contract after ASE commits an Editor graph."""
|
|
183
|
+
presentation_spec = {
|
|
184
|
+
"editor": editor,
|
|
185
|
+
"properties": [
|
|
186
|
+
{
|
|
187
|
+
"name": node.property_name,
|
|
188
|
+
"display_name": node.inspector_name,
|
|
189
|
+
"tooltip": node.tooltip,
|
|
190
|
+
"help": node.help,
|
|
191
|
+
"enabled_if": node.enabled_if,
|
|
192
|
+
}
|
|
193
|
+
for node in spec.nodes
|
|
194
|
+
if node.property_name is not None
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
apply_material_gui_spec(created, presentation_spec)
|
|
198
|
+
sync_compiled_property_metadata(created)
|
|
199
|
+
from asecli.core.compiled_metadata import hide_known_template_compiled_only_properties
|
|
200
|
+
|
|
201
|
+
hide_known_template_compiled_only_properties(created, spec.template.guid)
|
|
202
|
+
presentation = require_property_presentation(created)
|
|
203
|
+
return fix_checksum(created.serialize()), presentation
|
|
204
|
+
|
|
205
|
+
def _preserved_editor_asset_details(path: str, result: dict) -> dict:
|
|
206
|
+
"""Describe a post-commit failure without deleting an identity that may have changed."""
|
|
207
|
+
return {
|
|
208
|
+
"preserved_path": str(Path(path).resolve()),
|
|
209
|
+
"cleanup": "skipped_untrusted_post_commit_asset",
|
|
210
|
+
"transaction_nonce": result.get("transaction_nonce"),
|
|
211
|
+
"shader_sha256": result.get("shader_sha256"),
|
|
212
|
+
"meta_sha256": result.get("meta_sha256"),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _set_shader_name(ase_file: AseFile, name: str) -> None:
|
|
217
|
+
if not name.strip() or len(name) > 255:
|
|
218
|
+
raise CliError("USAGE_ERROR", "shader name must be 1-255 non-whitespace characters")
|
|
219
|
+
if any(ord(char) < 32 for char in name) or any(char in name for char in ('"', "'", ";", "\\")):
|
|
220
|
+
raise CliError("USAGE_ERROR", "shader name contains a forbidden quote, separator, escape, or control character")
|
|
221
|
+
matches = list(_SHADER_DECLARATION.finditer(ase_file.prefix))
|
|
222
|
+
if len(matches) != 1:
|
|
223
|
+
raise CliError("USAGE_ERROR", f"template must contain exactly one Shader declaration, found {len(matches)}")
|
|
224
|
+
declaration = matches[0]
|
|
225
|
+
ase_file.prefix = (
|
|
226
|
+
ase_file.prefix[: declaration.start("name")]
|
|
227
|
+
+ name
|
|
228
|
+
+ ase_file.prefix[declaration.end("name") :]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
master = main_master_node(ase_file.graph)
|
|
233
|
+
except ValueError as exc:
|
|
234
|
+
if any("MasterNode" in node.type_name for node in ase_file.graph.nodes):
|
|
235
|
+
raise CliError("USAGE_ERROR", f"cannot resolve graph Master name field: {exc}") from exc
|
|
236
|
+
return
|
|
237
|
+
index = _TEMPLATE_NAME_FIELD.get(master.type_name)
|
|
238
|
+
if index is None:
|
|
239
|
+
old_name = declaration.group("name")
|
|
240
|
+
candidates = [i for i, value in enumerate(master.raw_fields) if value == old_name]
|
|
241
|
+
if len(candidates) != 1:
|
|
242
|
+
raise CliError(
|
|
243
|
+
"USAGE_ERROR",
|
|
244
|
+
f"unsupported Master name layout for {master.type_name}: found {len(candidates)} candidate fields",
|
|
245
|
+
)
|
|
246
|
+
index = candidates[0]
|
|
247
|
+
if index >= len(master.raw_fields):
|
|
248
|
+
raise CliError("USAGE_ERROR", f"Master node {master.node_id} has no shader-name field {index}")
|
|
249
|
+
master.raw_fields[index] = name
|
|
250
|
+
ase_file.graph.replace_node(master)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""CLI composition for ASE CustomEditor and property metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..checks import fix_checksum, validate_file
|
|
9
|
+
from ..core import (
|
|
10
|
+
ASECLI_GUI_EDITOR,
|
|
11
|
+
MZGUI_EDITOR,
|
|
12
|
+
SUPPORTED_GUI_EDITORS,
|
|
13
|
+
apply_material_gui_spec,
|
|
14
|
+
enable_if_attribute,
|
|
15
|
+
graph_custom_editor,
|
|
16
|
+
inspect_custom_gui,
|
|
17
|
+
remove_property_metadata_attribute,
|
|
18
|
+
resolve_property_node,
|
|
19
|
+
semantic_attribute,
|
|
20
|
+
set_custom_editor,
|
|
21
|
+
set_property_metadata_attribute,
|
|
22
|
+
sync_compiled_property_metadata,
|
|
23
|
+
)
|
|
24
|
+
from .commands import CliError, _commit_text, _load
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_custom_gui(args) -> dict:
|
|
28
|
+
"""Inspect or safely mutate ASE CustomEditor and ASECLI property metadata."""
|
|
29
|
+
if args.enabled_if is not None and args.enabled_if_value is None:
|
|
30
|
+
raise CliError("USAGE_ERROR", "--enabled-if requires --enabled-if-value")
|
|
31
|
+
if args.enabled_if is None and args.enabled_if_value is not None:
|
|
32
|
+
raise CliError("USAGE_ERROR", "--enabled-if-value requires --enabled-if")
|
|
33
|
+
if args.enabled_if is None and args.enabled_if_operator is not None:
|
|
34
|
+
raise CliError("USAGE_ERROR", "--enabled-if-operator requires --enabled-if")
|
|
35
|
+
f = _load(args.file)
|
|
36
|
+
changes = []
|
|
37
|
+
node_actions = any(
|
|
38
|
+
(
|
|
39
|
+
args.group is not None,
|
|
40
|
+
args.clear_group,
|
|
41
|
+
args.tooltip is not None,
|
|
42
|
+
args.clear_tooltip,
|
|
43
|
+
args.help_box is not None,
|
|
44
|
+
args.clear_help_box,
|
|
45
|
+
args.enabled_if is not None,
|
|
46
|
+
args.clear_enabled_if,
|
|
47
|
+
bool(args.add_attribute),
|
|
48
|
+
bool(args.remove_attribute),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
single_mutation = node_actions or args.editor is not None or args.clear_editor
|
|
52
|
+
if args.spec and single_mutation:
|
|
53
|
+
raise CliError("USAGE_ERROR", "--spec cannot be combined with individual custom GUI mutation options")
|
|
54
|
+
if args.spec and (args.node is not None or args.property is not None):
|
|
55
|
+
raise CliError("USAGE_ERROR", "--spec cannot be combined with --node or --property")
|
|
56
|
+
if node_actions and args.node is None and args.property is None:
|
|
57
|
+
raise CliError("USAGE_ERROR", "--node or --property is required for ASECLI property metadata operations")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
initial_presentation = inspect_custom_gui(f)["property_presentation"]
|
|
61
|
+
if args.spec:
|
|
62
|
+
changes.extend(apply_material_gui_spec(f, _load_spec(args.spec)))
|
|
63
|
+
else:
|
|
64
|
+
if args.editor is not None:
|
|
65
|
+
changes.append(set_custom_editor(f, args.editor))
|
|
66
|
+
elif args.clear_editor:
|
|
67
|
+
changes.append(set_custom_editor(f, None))
|
|
68
|
+
|
|
69
|
+
target = None
|
|
70
|
+
if args.node is not None or args.property is not None:
|
|
71
|
+
target = resolve_property_node(
|
|
72
|
+
f.graph, node_id=args.node, property_name=args.property
|
|
73
|
+
).node_id
|
|
74
|
+
|
|
75
|
+
additions: list[str] = []
|
|
76
|
+
if args.group is not None:
|
|
77
|
+
additions.append(semantic_attribute("FoldoutMzgui", args.group))
|
|
78
|
+
if args.tooltip is not None:
|
|
79
|
+
additions.append(semantic_attribute("TooltipMzgui", args.tooltip))
|
|
80
|
+
if args.help_box is not None:
|
|
81
|
+
additions.append(semantic_attribute("HelpBoxMzgui", args.help_box))
|
|
82
|
+
if args.enabled_if is not None:
|
|
83
|
+
additions.append(
|
|
84
|
+
enable_if_attribute(
|
|
85
|
+
args.enabled_if,
|
|
86
|
+
args.enabled_if_operator or "Equal",
|
|
87
|
+
args.enabled_if_value,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
additions.extend(args.add_attribute or [])
|
|
91
|
+
|
|
92
|
+
if additions and graph_custom_editor(f.graph) not in SUPPORTED_GUI_EDITORS:
|
|
93
|
+
supported = ", ".join(sorted(SUPPORTED_GUI_EDITORS))
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"MZGUI-compatible property metadata requires a selected material GUI; "
|
|
96
|
+
f"pass --editor with one of: {supported}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
removals = (
|
|
100
|
+
[(args.clear_group, "FoldoutMzgui"), (args.clear_tooltip, "TooltipMzgui"),
|
|
101
|
+
(args.clear_help_box, "HelpBoxMzgui"),
|
|
102
|
+
(args.clear_enabled_if, "EnableIfMzgui")]
|
|
103
|
+
)
|
|
104
|
+
for enabled, type_name in removals:
|
|
105
|
+
if enabled:
|
|
106
|
+
changes.append(remove_property_metadata_attribute(f.graph, target, type_name))
|
|
107
|
+
for type_name in args.remove_attribute or []:
|
|
108
|
+
changes.append(remove_property_metadata_attribute(f.graph, target, type_name))
|
|
109
|
+
for raw in additions:
|
|
110
|
+
changes.append(set_property_metadata_attribute(f.graph, target, raw))
|
|
111
|
+
|
|
112
|
+
if changes and graph_custom_editor(f.graph) == ASECLI_GUI_EDITOR:
|
|
113
|
+
changes.insert(0, set_custom_editor(f, MZGUI_EDITOR))
|
|
114
|
+
if changes:
|
|
115
|
+
changes.extend(sync_compiled_property_metadata(f))
|
|
116
|
+
state = inspect_custom_gui(f)
|
|
117
|
+
except KeyError as exc:
|
|
118
|
+
raise CliError("NOT_FOUND", str(exc))
|
|
119
|
+
except ValueError as exc:
|
|
120
|
+
raise CliError("CUSTOM_GUI_ERROR", str(exc))
|
|
121
|
+
|
|
122
|
+
if not changes:
|
|
123
|
+
if args.write:
|
|
124
|
+
raise CliError("USAGE_ERROR", "--write requires a custom GUI mutation option")
|
|
125
|
+
return {"file": args.file, "action": "inspect", "state": state, "written": False}
|
|
126
|
+
|
|
127
|
+
contract_applies = initial_presentation["claimed"] or state["property_presentation"]["claimed"]
|
|
128
|
+
if args.write and contract_applies and not state["property_presentation"]["valid"]:
|
|
129
|
+
raise CliError(
|
|
130
|
+
"PROPERTY_PRESENTATION_ERROR",
|
|
131
|
+
"ASECLI-managed property presentation contract failed: "
|
|
132
|
+
+ ", ".join(state["property_presentation"]["violations"]),
|
|
133
|
+
{"property_presentation": state["property_presentation"]},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
issues = validate_file(f)
|
|
137
|
+
errors = [issue for issue in issues if issue["severity"] == "error"]
|
|
138
|
+
if errors:
|
|
139
|
+
raise CliError(
|
|
140
|
+
"VALIDATION_ERROR",
|
|
141
|
+
f"mutation would leave {len(errors)} structural error(s)",
|
|
142
|
+
{"issues": issues, "error_count": len(errors)},
|
|
143
|
+
)
|
|
144
|
+
output = fix_checksum(f.serialize())
|
|
145
|
+
if args.write:
|
|
146
|
+
_commit_text(args.file, output, f.source_digest)
|
|
147
|
+
return {
|
|
148
|
+
"file": args.file,
|
|
149
|
+
"action": "mutate",
|
|
150
|
+
"changes": changes,
|
|
151
|
+
"state": state,
|
|
152
|
+
"requires_recompile": bool(changes),
|
|
153
|
+
"written": bool(args.write),
|
|
154
|
+
"path": args.file if args.write else None,
|
|
155
|
+
"preview_bytes": len(output),
|
|
156
|
+
"checksum_recomputed": True,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _load_spec(path: str) -> dict:
|
|
161
|
+
try:
|
|
162
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
163
|
+
except FileNotFoundError as exc:
|
|
164
|
+
raise KeyError(f"spec file not found: {path}") from exc
|
|
165
|
+
except UnicodeDecodeError as exc:
|
|
166
|
+
raise ValueError(f"material GUI spec is not valid UTF-8: {path}") from exc
|
|
167
|
+
try:
|
|
168
|
+
return json.loads(text)
|
|
169
|
+
except json.JSONDecodeError as exc:
|
|
170
|
+
raise ValueError(f"invalid material GUI spec JSON at line {exc.lineno}, column {exc.colno}") from exc
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Choose the single material-GUI provider for an Editor create operation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..bridge import MZGUI_EDITOR, install_gui_support
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def select_editor_provider(shader_path: str) -> tuple[str, dict | None]:
|
|
9
|
+
"""Install the fallback only when the enclosing Unity project lacks MZGUI.
|
|
10
|
+
|
|
11
|
+
Projectless behavior exists solely for isolated bridge test doubles; a real
|
|
12
|
+
Editor create target must be inside a Unity project.
|
|
13
|
+
"""
|
|
14
|
+
project_root = unity_project_root(Path(shader_path).resolve())
|
|
15
|
+
if project_root is None:
|
|
16
|
+
return MZGUI_EDITOR, None
|
|
17
|
+
support = install_gui_support(project_root, write=True)
|
|
18
|
+
editor = support.get("recommended_editor")
|
|
19
|
+
if not isinstance(editor, str):
|
|
20
|
+
raise RuntimeError("material GUI provider selection did not return an editor")
|
|
21
|
+
return editor, support
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def unity_project_root(path: Path) -> Path | None:
|
|
25
|
+
current = path.parent
|
|
26
|
+
while current != current.parent:
|
|
27
|
+
if (current / "Assets").is_dir() and (current / "ProjectSettings").is_dir():
|
|
28
|
+
return current
|
|
29
|
+
current = current.parent
|
|
30
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""CLI composition for the built-in ASECLI Unity material GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from ..bridge import (
|
|
8
|
+
McpError,
|
|
9
|
+
handoff_gui_support,
|
|
10
|
+
install_gui_support,
|
|
11
|
+
probe_native_mzgui_via_mcp,
|
|
12
|
+
)
|
|
13
|
+
from .commands import CliError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_gui_support(args) -> dict:
|
|
17
|
+
if args.instance_token_argv is not None:
|
|
18
|
+
raise CliError("USAGE_ERROR", "do not pass MCP tokens via argv; use ASECLI_MCP_INSTANCE_TOKEN")
|
|
19
|
+
try:
|
|
20
|
+
runtime_probe = None
|
|
21
|
+
if args.runtime_probe:
|
|
22
|
+
runtime_probe = probe_native_mzgui_via_mcp(
|
|
23
|
+
args.project,
|
|
24
|
+
mcp_url=args.mcp_url,
|
|
25
|
+
instance_token=os.environ.get("ASECLI_MCP_INSTANCE_TOKEN"),
|
|
26
|
+
allow_remote_mcp=args.allow_remote_mcp,
|
|
27
|
+
)
|
|
28
|
+
operation = handoff_gui_support if args.handoff_native else install_gui_support
|
|
29
|
+
return operation(args.project, write=args.write, runtime_probe=runtime_probe)
|
|
30
|
+
except FileNotFoundError as exc:
|
|
31
|
+
raise CliError("NOT_FOUND", str(exc)) from exc
|
|
32
|
+
except McpError as exc:
|
|
33
|
+
raise CliError("BRIDGE_ERROR", str(exc), exc.data) from exc
|
|
34
|
+
except (FileExistsError, ValueError, RuntimeError, OSError) as exc:
|
|
35
|
+
raise CliError("GUI_SUPPORT_ERROR", str(exc)) from exc
|