Newer
Older
#!/usr/bin/env python3
'''
Application for procedural content adaption
*THIS IS VERY EXPERIMENTAL*
Contains a very poor man's implementation of spline mesh warping.
This application parses SVG path data of an outfit sample aligned to a body part.
The outfit is then replicated for other shapes of the same body part.
Example data is geared towards generating a strap outfit for boobs and torso
for all sizes of boobs and all shapes of torsos based on a single outfit for
boobs of size 2 and a hourglass torso respectively.
Limitations:
* handles paths only
* only svg.path Line and CubicBezier are tested
* only the aforementioned examples are tested
Usage:
python3 vector_clothing_replicator.py infile clothing bodypart destinationfile
Usage Example:
python3 vector_clothing_replicator.py vector_source.svg Straps Boob vector_destination.svg
python3 vector_clothing_replicator.py vector_source.svg Straps Torso vector_destination.svg
'''
from svg.path import parse_path
import copy
import lxml.etree as etree
import sys
REFERENCE_PATH_SAMPLES = 200
EMBED_REPLICATIONS = True # whether to embed all replications into the input file or output separate files
input_file = sys.argv[1]
clothing = sys.argv[2]
bodypart = sys.argv[3]
output_file_embed = sys.argv[4]
# TODO: make these configurable
output_file_pattern = '%s_%s_%s.svg' #bodypart, target_id, clothing
if ('Torso' == bodypart):
xpath_shape = './svg:g[@id="Torso_"]/svg:g[@id="Torso_%s"]/svg:path[@class="skin torso"]/@d' # TODO: formulate more general, independent of style
xpath_outfit_container = '//svg:g[@id="Torso_Outfit_%s_"]'%(clothing)
xpath_outfit = '//svg:g[@id="Torso_Outfit_%s_%s"]'%(clothing,'%s')
target_ids = "Unnatural,Hourglass,Normal".split(",")
reference_id = "Hourglass"
else:
raise RuntimeError("Please specify a bodypart for clothing to replicate.")
tree = etree.parse(input_file)
ns = {'svg' : 'http://www.w3.org/2000/svg'}
canvas = copy.deepcopy(tree)
for e in canvas.xpath('./svg:g',namespaces=ns)+canvas.xpath('./svg:path',namespaces=ns):
# TODO: this should be "remove all objects, preserve document properties"
e.getparent().remove(e)
def get_points(xpath_shape):
'''
Each path is used to sample a fixed number of points.
'''
paths_data = tree.xpath(xpath_shape,namespaces=ns)
points = []
path_length = None
for path_data in paths_data:
p = parse_path(path_data)
points += [
p.point(1.0/float(REFERENCE_PATH_SAMPLES)*i)
for i in range(REFERENCE_PATH_SAMPLES)
]
raise RuntimeError(
'No paths for reference points found by selector "%s".'%(xpath_shape)
)
return points
def point_movement(point, reference_points, target_points):
'''
For a given point, finds the nearest point in the reference path.
Gives distance vector from the nearest reference point to the
respective target reference point.
'''
distances = [abs(point-reference_point) for reference_point in reference_points]
min_ref_dist_idx = min(enumerate(distances), key=lambda x:x[1])[0]
movement = target_points[min_ref_dist_idx] - reference_points[min_ref_dist_idx]
return movement
reference_points = get_points(xpath_shape%(reference_id))
container = tree.xpath(xpath_outfit_container,namespaces=ns)
if (len(container) != 1):
raise RuntimeError('Outfit container selector "%s" does not yield exactly one layer.'%(xpath_outfit_container))
container = container[0]
outfit_source = container.xpath(xpath_outfit%(reference_id),namespaces=ns)
if (len(outfit_source) != 1):
raise RuntimeError('Outfit source selector "%s" does not yield exactly one outfit layer in container selected by "%s".'%(xpath_outfit%(reference_id), xpath_outfit_container))
outfit_source = outfit_source[0]
for target_id in target_ids:
print(
'Generating variant "%s" of clothing "%s" for bodypart "%s"...'%
(target_id, clothing, bodypart)
)
outfit = copy.deepcopy(outfit_source)
paths = outfit.xpath('./svg:path',namespaces=ns)
if target_id == reference_id:
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
layerid = outfit.get('id').replace('_%s'%(reference_id),'_%s'%(target_id))
outfit.set('id', layerid)
outfit.set(etree.QName('http://www.inkscape.org/namespaces/inkscape', 'label'), layerid) # for the Inkscape-users
target_points = get_points(xpath_shape%(target_id))
if (len(reference_points) != len(target_points)):
raise RuntimeError(
('Different amounts of sampled points in reference "%s" and target "%s" paths. '+
'Selector "%s" probably matches different number of paths in the two layers.')%
(reference_id, target_id, xpath_shape)
)
for path in paths:
path_data = path.get("d")
p = parse_path(path_data)
for segment in p:
original_distance = abs(segment.end-segment.start)
start_movement = point_movement(segment.start, reference_points, target_points)
segment.start += start_movement
end_movement = point_movement(segment.end, reference_points, target_points)
segment.end += end_movement
distance = abs(segment.end-segment.start)
try:
# enhance position of CubicBezier control points
# amplification is relative to the distance gained by movement
segment.control1 += start_movement
segment.control1 += (segment.control1-segment.start)*(distance/original_distance-1.0)
segment.control2 += end_movement
segment.control2 += (segment.control2-segment.end)*(distance/original_distance-1.0)
except AttributeError as ae:
# segment is not a CubicBezier
pass
path.set("d", p.d())
if EMBED_REPLICATIONS:
container.append(outfit)
container = copy.deepcopy(canvas).xpath('.',namespaces=ns)[0]
container.append(outfit)
svg = etree.tostring(container, pretty_print=True)
with open((output_file_pattern%(bodypart, target_id, clothing)).lower(), 'wb') as f:
f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
f.write(svg)
if EMBED_REPLICATIONS:
svg = etree.tostring(tree, pretty_print=True)
with open(output_file_embed, 'wb') as f: