Files
AnyImage/tools/nodes/preview/export.py
nan dc000e50b1 feat: Implement weight cutout boundary smoothing for image depth cutout
- Added a new proposal for boundary smoothing in the weight cutout process, differentiating between Split edges and Outline smoothing requirements.
- Introduced a new capability `cutout-boundary-smoothing` to control smoothing strength and default values.
- Updated specifications to define the behavior of boundary-specific smoothing strength and default settings.
- Created tasks to verify the implementation of boundary behavior and asset updates.
- Developed comprehensive tests for depth symmetry and normal scaling to ensure expected behavior and performance.
- Introduced new attributes for normal mapping to facilitate persistent face attributes sharing between depth symmetry and image materials.
- Built a new node group for O Image Depth Symmetry, integrating the new smoothing capabilities and ensuring compatibility with existing workflows.
2026-09-14 12:44:20 +08:00

141 lines
6.8 KiB
Python

"""Preview node groups with the socket dimension model in a self-contained HTML file."""
import argparse
import json
from pathlib import Path
import bpy
from ..arrangement.dimensions import estimate_node_dimensions
from ..arrangement.flow import node_roles
from ..arrangement.connections import link_endpoints
from ..arrangement.zones import node_zones, repeat_regions
def _display_value(value):
if isinstance(value, bpy.types.ID):
return value.name
if isinstance(value, str):
return value
if isinstance(value, bool):
return str(value)
if isinstance(value, (int, float)):
return str(round(value, 4))
if value is None:
return ""
try:
return ", ".join(str(round(x, 3)) for x in value)
except TypeError:
return str(value)
def _node_group_data(node_group):
nodes = [node for node in node_group.nodes if node.bl_idname != "NodeFrame"]
ids = {node: index for index, node in enumerate(nodes)}
dimensions = {node: estimate_node_dimensions(node) for node in nodes}
roles = node_roles(node_group)
records = []
for node in nodes:
size = dimensions[node]
settings = []
for name in ("operation", "data_type", "input_type", "domain", "mode", "pattern_mode"):
if not hasattr(node, name):
continue
value = getattr(node, name)
prop = node.bl_rna.properties[name]
label = prop.enum_items[value].name if prop.type == "ENUM" and value in prop.enum_items else str(value)
settings.append(label)
title = node.bl_label
if node.bl_idname in {"GeometryNodeGroup", "ShaderNodeGroup"}:
title = node.node_tree.name
elif hasattr(node, "operation") and settings:
title = settings[0]
sockets = []
for socket, offset in size.sockets.items():
default = _display_value(socket.default_value) if not socket.is_linked and hasattr(socket, "default_value") else ""
sockets.append([socket.name, round(offset, 2), int(socket.is_output), socket.type, default])
records.append({"id": ids[node], "name": node.name, "title": title,
"type": node.bl_idname, "role": roles[node],
"x": round(node.location_absolute.x, 2), "y": round(-node.location_absolute.y, 2),
"w": size.width, "h": size.height, "folded": bool(node.hide),
"settings": settings, "sockets": sockets})
links = []
positions = {node: tuple(node.location_absolute) for node in nodes}
for link in node_group.links:
source, target = link.from_node, link.to_node
(x1, y1), (x2, y2) = link_endpoints(link, positions, dimensions)
links.append([ids[source], ids[target], *[round(v, 2) for v in (x1, -y1, x2, -y2)], link.from_socket.type])
zones = []
repeats = {frozenset(members): box for members, box in repeat_regions(node_group, positions, dimensions)}
for entry, end, members in node_zones(node_group):
if entry.bl_idname != "GeometryNodeRepeatInput":
members = {node for node in members if roles[node] == "geometry"} | {entry, end}
boxes = [records[ids[node]] for node in members]
padding = 60 if entry.bl_idname == "GeometryNodeRepeatInput" else 40
left = min(node["x"] for node in boxes) - padding
top = min(node["y"] for node in boxes) - 60
right = max(node["x"] + node["w"] for node in boxes) + padding
bottom = max(node["y"] + node["h"] for node in boxes) + 40
if entry.bl_idname == "GeometryNodeRepeatInput":
left, right, upper, lower = repeats[frozenset(members)]
top, bottom = -upper, -lower
zones.append({"title": entry.bl_label.removesuffix(" Input"),
"type": entry.bl_idname, "entry": ids[entry], "exit": ids[end],
"nodes": sorted(ids[node] for node in members),
"x": left, "y": top, "w": right - left, "h": bottom - top})
zones.sort(key=lambda zone: zone["w"] * zone["h"], reverse=True)
frames = [{"title": node.label or node.name, "x": node.location_absolute.x,
"y": -node.location_absolute.y, "w": node.width, "h": node.height,
"nodes": [ids[child] for child in nodes if child.parent == node]}
for node in node_group.nodes if node.bl_idname == "NodeFrame"]
return {"name": node_group.name, "nodes": records, "links": links, "zones": zones, "frames": frames}
def write_node_preview(node_groups, output_path, *, fragment=False):
"""Write an interactive preview without changing the supplied node groups."""
data = [_node_group_data(node_group) for node_group in node_groups]
if not data or any(not node_group["nodes"] for node_group in data):
raise ValueError("The preview requires nonempty node groups")
payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
markup = Path(__file__).with_name("template.html").read_text(encoding="utf-8").replace("__NODE_DATA__", payload)
if not fragment:
markup = """<!doctype html>
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>节点排列预览</title>
<style>
:root { color-scheme: dark; --background: #191919; --foreground: #ededed;
--muted-foreground: #a0a0a0; --border: #404040; }
body { margin: 24px; background: var(--background); color: var(--foreground); font: 14px system-ui,sans-serif; }
h3 { font-size: 16px; font-weight: 500; }
.viz-controls,.viz-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 8px 0; }
.form-label { display: flex; align-items: center; gap: 8px; }
button,select { padding: 8px; font: inherit; max-width: 100%; }
.text-small { font-size: 12px; }
@media(max-width:440px) { body { margin: 12px; } .form-label { width: 100%; } select { flex: 1; min-width: 0; } }
</style><body>
""" + markup + "\n</body></html>"
output_path = Path(output_path).resolve()
output_path.write_text(markup, encoding="utf-8")
return output_path
def main():
"""Build and arrange node assets in memory for preview."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True, help="Output HTML path")
parser.add_argument("--fragment", action="store_true", help="Write a fragment for an inline preview")
args = parser.parse_args()
output_path = args.output.resolve()
from ..build import build_node_groups
from ..arrangement.arrange import arrange_nodes
bpy.ops.wm.read_factory_settings(use_empty=True)
node_groups = build_node_groups()
for node_group in node_groups:
arrange_nodes(node_group)
print(write_node_preview(node_groups, output_path, fragment=args.fragment))
if __name__ == "__main__":
main()