feat: Add new Geometry Nodes groups for image relief and shadeless shader

- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters.
- Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections.
- Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format.
- Added a template HTML file for rendering node previews with customizable views and controls.
This commit is contained in:
2026-09-09 21:24:57 +08:00
parent f5aff50b13
commit 89087fc2e2
118 changed files with 5635 additions and 3130 deletions

View File

@@ -396,43 +396,23 @@ class ClipboardImageTest(unittest.TestCase):
finally:
clipboard_image._native_copy_token = original_token
def test_shadeless_nodes_mix_emission_with_transparency(self):
created_nodes = {}
def test_plane_paste_uses_shared_material_for_both_shading_modes(self):
from anyimage.operators.convert_to_plane import image_plane
def create_node(node_type):
node = SimpleNamespace(location=None, inputs={}, outputs={})
if node_type == "ShaderNodeBsdfTransparent":
node.outputs["BSDF"] = object()
elif node_type == "ShaderNodeEmission":
node.inputs["Color"] = object()
node.outputs["Emission"] = object()
else:
node.inputs = [object(), object(), object()]
node.outputs["Shader"] = object()
created_nodes[node_type] = node
return node
nodes = SimpleNamespace(new=Mock(side_effect=create_node))
links = SimpleNamespace(new=Mock())
image_node = SimpleNamespace(
outputs={"Color": object(), "Alpha": object()},
)
output_node = SimpleNamespace(inputs={"Surface": object()})
self.actions._create_shadeless_nodes(
nodes,
links,
image_node,
output_node,
)
mix_node = created_nodes["ShaderNodeMixShader"]
links.new.assert_any_call(image_node.outputs["Alpha"], mix_node.inputs[0])
links.new.assert_any_call(
created_nodes["ShaderNodeEmission"].outputs["Emission"],
mix_node.inputs[2],
)
self.assertEqual(links.new.call_count, 5)
image = SimpleNamespace(name="Clipboard", size=(16, 8))
context = SimpleNamespace(mode="OBJECT", scene=object())
for shadeless in (False, True):
with (
patch.object(self.actions, "create_image_material", return_value="Material") as create_material,
patch.object(image_plane, "create_image_plane_object") as create_plane,
patch.object(self.actions, "_place_in_view"),
patch.object(self.actions, "_select_only"),
):
self.actions.add_image_plane(context, image, shadeless=shadeless)
create_material.assert_called_once_with(
image, image, shadeless=shadeless, scene=context.scene,
)
self.assertEqual(create_plane.call_args.args[3], "Material")
def test_reference_import_routes_to_image_empty(self):
context = self.make_context("VIEW_3D")