Source code for svg_path_editor.path_change_origin
# This file is part of https://github.com/KurtBoehm/svg-path-editor.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations
from .path_operations import optimize_path
from .sub_path_bounds import get_sub_path_bounds
from .svg import SvgItem, SvgPath
[docs]
def change_path_origin(
svg: SvgPath, new_origin_index: int, subpath: bool | None = None
) -> SvgPath:
"""
Return a new path where the origin of a (sub)path is moved.
The command at ``new_origin_index`` becomes the first command of the
affected subpath segment; all items of that subpath are rotated
accordingly.
If ``subpath`` is ``True``, only the subpath containing ``new_origin_index`` is
transformed; if ``False``/``None``, the whole path is treated as a single segment.
:param svg: Original path to transform.
:param new_origin_index:
Index of the command that should become the new origin within its subpath.
:param subpath:
If ``True``, restrict the change to the subpath containing ``new_origin_index``;
if ``False`` or ``None``, treat the full path segment as one subpath.
:return: A new :class:`~svg_path_editor.SvgPath` instance with the
origin moved and the path representation optimized.
"""
if len(svg.path) <= new_origin_index or new_origin_index == 0:
# Nothing to change or invalid index; just return a clone.
return svg.clone()
new_svg = svg.clone()
path = new_svg.path
start, end = get_sub_path_bounds(new_svg, new_origin_index if subpath else None)
segment_len = end - start
is_before_relative = end < len(path) and path[end].relative
if is_before_relative:
# Make following item absolute to simplify rewrites.
path[end].relative = False
new_first_item = path[new_origin_index]
new_last_item = path[new_origin_index - 1]
# Shorthands must be converted to explicit forms before becoming new origin.
match new_first_item.get_type().upper():
case "S":
new_svg.change_type(
new_origin_index,
"c" if new_first_item.relative else "C",
)
case "T":
new_svg.change_type(
new_origin_index,
"q" if new_first_item.relative else "Q",
)
case _:
pass
# Z that comes after new origin must be converted to L, up to the next M.
for i in range(new_origin_index, end):
item = path[i]
match item.get_type().upper():
case "Z":
new_svg.change_type(i, "L")
case "M":
break
case _:
pass
output_path: list[SvgItem] = []
sub_path = path[start:end]
first_item = sub_path[0]
last_item = sub_path[segment_len - 1]
for i in range(segment_len):
if i == 0:
# Insert a new M at the origin of the previous item.
new_origin = new_last_item.target_location
item = SvgItem.make(["M", str(new_origin.x), str(new_origin.y)])
output_path.append(item)
if new_origin_index + i == start + segment_len:
# We may be able to remove the initial M if last item has the same target.
tg1 = first_item.target_location
tg2 = last_item.target_location
if tg1.x == tg2.x and tg1.y == tg2.y:
following_m = next(
(
idx
for idx, it in enumerate(sub_path)
if idx > 0 and it.get_type().upper() == "M"
),
-1,
)
first_z = next(
(
idx
for idx, it in enumerate(sub_path)
if it.get_type().upper() == "Z"
),
-1,
)
if first_z == -1 or (following_m != -1 and first_z > following_m):
# We can remove initial M if there is no Z in the following subpath.
continue
output_path.append(sub_path[(new_origin_index - start + i) % segment_len])
new_svg.path = [*path[:start], *output_path, *path[end:]]
new_svg.refresh_absolute_positions()
if is_before_relative:
# Restore relativity of the first item after the modified segment.
new_svg.path[start + len(output_path)].relative = True
# Optimize representation of the resulting path.
return optimize_path(
new_svg,
remove_useless_commands=True,
use_shorthands=True,
use_close_path=True,
)