Mirror vertices in region layers

Hello,

I just made my first Synfig script. I have the need to make symmetrical clothes, but I don’t want to draw both sides and I need it to be exactly symmetrical.

I made these half dresses for the example. One of them contains “mirrorthis” in the description.

When the python script runs, Synfig passes the filename, an uncompressed xml file, which I open with the etree native python library.

I filter the layers with the word “mirrorthis”, then I drill down with “bline” xml tags, and “vector”, “t1”, “t2”.

I invert the x coordinate of vertex, and rewrite each angle with its complementary (180-angle).

Finally I write the tree into the filename with the xml library.

When Synfig re-opens the file, the dress with the word “mirrorthis” appears mirrored.

But I don’t just want to create mirrored versions and place it next to the original. I want to duplicate the vertices, and concatenate them in the correct order, to make a single path for the whole dress/clothes. I think I need to reverse the order of the duplicated vertices, so the resulting shape is correct.

It’s also the first time I use the xml library, so I need the help. Please send me a code example to duplicate and reverse XML tags.

I plan to work on the math, so that the shape is not simply mirrored around x=0, but mirrored around the line that connects the first and last vertex.

Attached: plugin script and half-dress design

Make mirror nodes.zip (3.4 KB)

Reference: xml.etree.ElementTree — The ElementTree XML API — Python 3.14.5 documentation

3 Likes

Appart the fact that you want to do a single spline, is there any other reason not to use a nested group with a -1 horizontal scale transformation? (classical Synfig way for mirror)

Also, there is another way than the simple “classical” Synfig way
Doing almost the same structure, remove all the vertices of the mirrored copy in the nested group.
Then export the vertices list from the original layer.
In the nested layer, connect the empty vertices list to the one in the Library.
From now on, you can alter any of the vertices, it will be automatically mirrored.
You can also export/connect the color if you plan to change it.

See here:
dress_mirrored_classical_and_exported_vertices.zip (4.4 KB)

It would be easier to draw the first half vertically or horizontally with a nested mirrored group, then apply a rotation to the parent group!

1 Like

thank you

i want it to be one single path because the dress will start symmetrical, but i want to deform it continuously into 3/4 view, wind blowing to the side and other asymmetrical positions.

And maybe control the rotation with an on-screen joystick.

I know that the xml library can do it… i need to try something else tonight

2 Likes

Oh my, one more PR I didn’t finish due to other priorities (port layers to Cobra renderer, port stuff to GTK3 actions, etc.) and my crazy mind:

You can get the code idea there.

2 Likes

That’s cool :slight_smile:

In my case, I want to fine tune the shape of one half so that it fits the body and looks nice… then I run my plugin, and the open curve will turn into a closed curve, with symmetry. Then the curve will be fully editable like an outline or region

	 layers = root_file.findall(“.//*[@group=‘mirror’]”) # set layer with name ‘mirror’

     for bl in layers:
		bline = bl.find(".//*[@name='bline']")

		if bline is None:
			print("no bline")
			continue

		mirrored_entries = []

		for entry in bline.findall(".//entry"):
			entry_c = copy.deepcopy(entry) # copy element

	        # mirror vector x
			vec = entry_c.find(".//point/vector")
			vec[0].text = str(float(vec[0].text) * -1)

	        # mirror theta angle
			angle = entry_c.find(".//theta/angle")
			angle.set("value", str(float(angle.get("value")) * -1))

			mirrored_entries.append(entry_c)

		for entry in reversed(mirrored_entries): # reverse list entry
			entry[0].attrib.pop("guid", None)
			bline[0].append(entry)
2 Likes

Thank you for sending me the code suggestion. I only had a little time to try it.

I fixed a couple of things to make it work:

import copy

root → root_file

group= → desc=

I almost got the mirrored path :slight_smile: some tangents have reversed directions

… actually it only happens at the vertices with non-symmetrical tangents (cusps)… so I think that the tangents should be swapped in the new reflected nodes

Thank you! This is still in progress!

Make mirror nodes 2.zip (3.5 KB)

1 Like

layers_groups = root_file.findall(“.//*[@group=‘mirror’]”)

if not layers_groups:
	print(" !!! set layer with name 'mirror' not found")
	return

def negate_angle(angle):
	angle.set("value", str(-float(angle.get("value"))))

for group in layers_groups:
	for bline in group.findall(".//*[@name='bline']"):

		mirrored_entries = []

		for entry in bline.findall(".//entry"):
			entry_c = copy.deepcopy(entry)

			# mirror vector X
			vec = entry_c.find(".//point/vector")
			if vec is not None:
				vec[0].text = str(-float(vec[0].text))

			split_angle = entry_c.find(".//split_angle/bool")
			split_radius = entry_c.find(".//split_radius/bool")

			is_split_angle = split_angle.get("value") == "true"
			is_split_radius = split_radius.get("value") == "true"

			if is_split_angle:
				angle_t2 = entry_c.find(".//t2//theta/angle")
				angle_t1 = entry_c.find(".//t1//theta/angle")

				negate_angle(angle_t1)
				negate_angle(angle_t2)

				if not is_split_radius: # sometime radius not update, fill manually
					radius_t1 = entry_c.find(".//t1//radius/real")
					radius_t2 = entry_c.find(".//t2//radius/real")
					radius_t2.set("value", radius_t1.get("value"))

				t1 = entry_c.find(".//t1")
				t2 = entry_c.find(".//t2")

				t1_child = copy.deepcopy(t1[0])
				t2_child = copy.deepcopy(t2[0])

				t1[:] = [t2_child]
				t2[:] = [t1_child]

			else:
				negate_angle(entry_c.find(".//theta/angle"))

				if is_split_radius:
					r1 = entry_c.find(".//t1//radius/real")
					r2 = entry_c.find(".//t2//radius/real")
					r1.attrib["value"], r2.attrib["value"] = (
						r2.attrib["value"],
						r1.attrib["value"],
					)

			mirrored_entries.append(entry_c)

		for entry in reversed(mirrored_entries):
			entry[0].attrib.pop("guid", None)
			bline[0].append(entry)
2 Likes