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.
This commit is contained in:
2026-09-14 12:44:20 +08:00
parent 09778c4b48
commit dc000e50b1
53 changed files with 1350 additions and 364 deletions

View File

@@ -46,7 +46,7 @@ def _node_group_data(node_group):
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 == "GeometryNodeGroup":
if node.bl_idname in {"GeometryNodeGroup", "ShaderNodeGroup"}:
title = node.node_tree.name
elif hasattr(node, "operation") and settings:
title = settings[0]
@@ -120,7 +120,7 @@ button,select { padding: 8px; font: inherit; max-width: 100%; }
def main():
"""Build and arrange geometry assets in memory for preview."""
"""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")
@@ -130,7 +130,7 @@ def main():
from ..arrangement.arrange import arrange_nodes
bpy.ops.wm.read_factory_settings(use_empty=True)
node_groups = [node_group for node_group in build_node_groups() if node_group.bl_idname == "GeometryNodeTree"]
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))

View File

@@ -27,7 +27,7 @@
</div>
<canvas id="layout-canvas" role="img" aria-label="按实际排列坐标绘制的节点组,尺寸和曲线使用估算模型"></canvas>
<div class="viz-row text-small" id="layout-legend">
<span><i class="geometry-key"></i>几何连接</span>
<span><i class="geometry-key"></i>几何与着色连接</span>
<span><i class="field-key"></i>字段与参数连接</span>
<span id="layout-count"></span>
</div>
@@ -113,15 +113,15 @@
return;
}
if (view.value === 'middle') {
const geometryNodes = current.nodes.filter(n => n.type !== 'NodeReroute' && n.role === 'geometry');
const midX = (Math.min(...geometryNodes.map(n => n.x)) + Math.max(...geometryNodes.map(n => n.x))) / 2;
const target = geometryNodes.sort((a, b) => Math.abs(a.x - midX) - Math.abs(b.x - midX))[0];
const mainNodes = current.nodes.filter(n => n.type !== 'NodeReroute' && n.role !== 'field');
const midX = (Math.min(...mainNodes.map(n => n.x)) + Math.max(...mainNodes.map(n => n.x))) / 2;
const target = mainNodes.sort((a, b) => Math.abs(a.x - midX) - Math.abs(b.x - midX))[0];
fit([target.x - 850, target.y - 560, target.x + 670, target.y + 600]);
detail.textContent = `${current.name} · 中段计算局部`;
return;
}
if (view.value === 'input') {
const target = current.nodes.filter(n => n.role === 'geometry' && n.type !== 'NodeReroute').sort((a, b) => a.x - b.x)[0];
const target = current.nodes.filter(n => n.role !== 'field' && n.type !== 'NodeReroute').sort((a, b) => a.x - b.x)[0];
fit([target.x - 850, target.y - 650, target.x + 750, target.y + 650]);
detail.textContent = `${current.name} · 起点输入附近`; return;
}
@@ -192,9 +192,9 @@
const x2 = wx2 * scale + dx, y2 = wy2 * scale + dy;
const bend = Math.max(40, Math.abs(wx2 - wx1) * 0.42) * scale;
const active = selected === source || selected === target;
context.globalAlpha = selected !== null && !active ? 0.18 : type === 'GEOMETRY' ? 0.85 : 0.46;
context.strokeStyle = type === 'GEOMETRY' ? geometry : fields;
context.lineWidth = active ? 2.5 : type === 'GEOMETRY' ? 1.35 : 0.8;
context.globalAlpha = selected !== null && !active ? 0.18 : ['GEOMETRY', 'SHADER'].includes(type) ? 0.85 : 0.46;
context.strokeStyle = ['GEOMETRY', 'SHADER'].includes(type) ? geometry : fields;
context.lineWidth = active ? 2.5 : ['GEOMETRY', 'SHADER'].includes(type) ? 1.35 : 0.8;
context.beginPath(); context.moveTo(x1, y1);
context.bezierCurveTo(x1 + bend, y1, x2 - bend, y2, x2, y2); context.stroke();
}
@@ -202,9 +202,9 @@
for (const node of current.nodes) {
const x = node.x * scale + dx, y = node.y * scale + dy, w = node.w * scale, h = node.h * scale;
if (x + w < 0 || x > width || y + h < 0 || y > height) continue;
const geometryNode = node.type === 'NodeReroute'
? node.sockets.some(socket => socket[3] === 'GEOMETRY') : node.role === 'geometry';
const hue = geometryNode ? geometry : other;
const mainNode = node.type === 'NodeReroute'
? node.sockets.some(socket => ['GEOMETRY', 'SHADER'].includes(socket[3])) : node.role !== 'field';
const hue = mainNode ? geometry : other;
if (node.type === 'NodeReroute') {
context.fillStyle = hue; context.beginPath();
context.arc(x + w / 2, y + h / 2, Math.max(1.5, w / 3), 0, Math.PI * 2); context.fill(); continue;
@@ -241,7 +241,7 @@
}
if (scale >= 0.22) {
for (const socket of node.sockets) {
context.fillStyle = socket[3] === 'GEOMETRY' ? geometry : fields;
context.fillStyle = ['GEOMETRY', 'SHADER'].includes(socket[3]) ? geometry : fields;
context.beginPath(); context.arc(socket[2] ? x + w : x, y + socket[1] * scale,
Math.max(1.4, 3.6 * scale), 0, Math.PI * 2); context.fill();
}
@@ -285,7 +285,7 @@
const x = (event.clientX - rect.left - dx) / scale, y = (event.clientY - rect.top - dy) / scale;
const node = current.nodes.findLast(n => x >= n.x && x <= n.x + n.w && y >= n.y && y <= n.y + n.h);
selected = node ? node.id : null;
detail.textContent = node ? `${node.name} · ${node.title} · ${node.role === 'geometry' ? '几何主线' : '计算或输入'}${node.settings.length ? ' · ' + node.settings.join(' / ') : ''}` : current.name;
detail.textContent = node ? `${node.name} · ${node.title} · ${node.role !== 'field' ? '结果主线' : '计算或输入'}${node.settings.length ? ' · ' + node.settings.join(' / ') : ''}` : current.name;
draw();
}
dragging = null;