Files
nodewars/addons/curved_lines_2d/svg_import_tab.gd
2026-05-13 18:52:00 +02:00

989 lines
44 KiB
GDScript

@tool
extends Control
# Fraction of a radius for a bezier control point
const R_TO_CP = 0.5523
const PLC_EXP = "__PLC_EXP__"
const SVG_ROOT_META_NAME := "svg_root"
const SVG_STYLE_META_NAME := "svg_style"
const PAINT_ORDER_MAP := {
"normal": ['add_fill_to_path', 'add_stroke_to_path', 'add_collision_to_path'],
"fill stroke markers": ['add_fill_to_path', 'add_stroke_to_path', 'add_collision_to_path'],
"stroke fill markers": ['add_stroke_to_path', 'add_fill_to_path', 'add_collision_to_path'],
"fill markers stroke": ['add_fill_to_path', 'add_collision_to_path', 'add_stroke_to_path'],
"markers fill stroke": ['add_collision_to_path', 'add_fill_to_path', 'add_stroke_to_path'],
"stroke markers fill": ['add_stroke_to_path', 'add_collision_to_path', 'add_fill_to_path'],
"markers stroke fill": ['add_collision_to_path', 'add_stroke_to_path', 'add_fill_to_path']
}
const STROKE_CAP_MAP := {
"butt": Line2D.LineCapMode.LINE_CAP_NONE,
"round": Line2D.LineCapMode.LINE_CAP_ROUND,
"square": Line2D.LineCapMode.LINE_CAP_BOX
}
const STROKE_JOINT_MAP := {
"miter": Line2D.LineJointMode.LINE_JOINT_SHARP,
"miter-clip": Line2D.LineJointMode.LINE_JOINT_SHARP,
"round": Line2D.LineJointMode.LINE_JOINT_ROUND,
"bevel": Line2D.LineJointMode.LINE_JOINT_BEVEL,
"arc": Line2D.LineJointMode.LINE_JOINT_SHARP
}
enum LogLevel { DEBUG, INFO, WARN, ERROR }
var error_label_settings : LabelSettings = null
var warning_label_settings : LabelSettings = null
var info_label_settings : LabelSettings = null
var debug_label_settings : LabelSettings = null
## Settings
var collision_object_type := ScalableVectorShape2D.CollisionObjectType.NONE
var import_as_svs := true
var lock_shapes := true
var antialiased_shapes := false
var import_stroke_as_line_2d := true
var import_file_dialog : EditorFileDialog = null
var warning_dialog : AcceptDialog = null
var undo_redo : EditorUndoRedoManager = null
var LinkButtonScene : PackedScene = null
func _enter_tree() -> void:
error_label_settings = preload("res://addons/curved_lines_2d/error_label_settings.tres")
warning_label_settings = preload("res://addons/curved_lines_2d/warn_label_settings.tres")
info_label_settings = preload("res://addons/curved_lines_2d/info_label_settings.tres")
debug_label_settings = preload("res://addons/curved_lines_2d/debug_label_settings.tres")
LinkButtonScene = preload("res://addons/curved_lines_2d/link_button_with_copy_hint.tscn")
%LogScrollContainer.get_v_scroll_bar().connect("changed", func(): %LogScrollContainer.scroll_vertical = %LogScrollContainer.get_v_scroll_bar().max_value )
import_file_dialog = EditorFileDialog.new()
import_file_dialog.add_filter("*.svg", "SVG image")
import_file_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
import_file_dialog.file_selected.connect(_load_svg)
EditorInterface.get_base_control().add_child(import_file_dialog)
undo_redo = EditorInterface.get_editor_undo_redo()
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
if not typeof(data) == TYPE_DICTIONARY and "type" in data and data["type"] == "files":
return false
for file : String in data["files"]:
if file.ends_with(".svg"):
return true
return false
func log_message(msg : String, log_level : LogLevel = LogLevel.INFO) -> void:
var lbl := Label.new()
match log_level:
LogLevel.ERROR:
warning_dialog.dialog_text = msg
warning_dialog.popup_centered()
lbl.label_settings = error_label_settings
LogLevel.WARN:
lbl.label_settings = warning_label_settings
LogLevel.DEBUG:
lbl.label_settings = debug_label_settings
LogLevel.INFO,_:
lbl.label_settings = info_label_settings
lbl.text = msg
%ImportLogContainer.add_child(lbl)
func _drop_data(at_position: Vector2, data: Variant) -> void:
if _can_drop_data(at_position, data):
_load_svg(data["files"][0])
func _get_viewport_center() -> Vector2:
var tr := EditorInterface.get_editor_viewport_2d().global_canvas_transform
var og := tr.get_origin()
var sz := Vector2(EditorInterface.get_editor_viewport_2d().size)
return (sz / 2) / tr.get_scale() - og / tr.get_scale()
func _load_svg(file_path : String) -> void:
for child in %ImportLogContainer.get_children():
child.queue_free()
var xml_parser = XMLParser.new()
var scene_root := EditorInterface.get_edited_scene_root()
var selected_nodes := EditorInterface.get_selection().get_selected_nodes()
var parent_node := scene_root if selected_nodes.is_empty() else selected_nodes[0]
if not scene_root is Node:
log_message("ERROR: Can only import into an opened scene", LogLevel.ERROR)
return
if xml_parser.open(file_path) != OK:
log_message("ERROR: Failed to open %s for reading" % file_path, LogLevel.ERROR)
return
log_message("Importing SVG file: %s" % file_path, LogLevel.INFO)
var svg_root := Node2D.new()
svg_root.name = file_path.get_file().replace(".svg", "").to_pascal_case()
undo_redo.create_action("Import SVG file as Nodes: %s" % svg_root.name)
svg_root.set_meta(SVG_ROOT_META_NAME, true)
_managed_add_child_and_set_owner(parent_node, svg_root, scene_root)
var current_node := svg_root
var svg_gradients : Array[Dictionary] = []
var svg_xml_node : SVGXMLElement = parse_svg_xml_file(xml_parser)
process_svg_xml_tree(svg_xml_node, scene_root, svg_root, current_node, svg_gradients)
undo_redo.commit_action(false)
if not import_as_svs:
await RenderingServer.frame_post_draw
Line2DGeneratorInspectorPlugin._copy_baked_node(svg_root, parent_node, scene_root)
parent_node.remove_child(svg_root)
log_message("Import finished.\n\nThe SVG importer is still incrementally improving (slowly).")
var link_button = LinkButtonScene.instantiate()
link_button.text = "Click here to report issues or improvement requests on github"
link_button.uri = "https://github.com/Teaching-myself-Godot/ez-curved-lines-2d/issues"
%ImportLogContainer.add_child(link_button)
var selection_target = (
svg_root.find_children("*", "ScalableVectorShape2D")
.filter(func(n : CanvasItem): return n.is_visible_in_tree()).pop_front()
)
if not is_instance_valid(selection_target):
selection_target = svg_root
EditorInterface.call_deferred('edit_node', selection_target)
await get_tree().create_timer(0.0167).timeout
EditorInterface.get_editor_viewport_2d().get_parent().grab_focus()
var key_ev := InputEventKey.new()
key_ev.keycode = KEY_F
key_ev.pressed = true
Input.parse_input_event(key_ev)
func parse_svg_xml_file(xml_parser : XMLParser) -> SVGXMLElement:
var svg_xml_node : SVGXMLElement = null
while xml_parser.read() == OK:
if not xml_parser.get_node_type() in [XMLParser.NODE_ELEMENT, XMLParser.NODE_ELEMENT_END]:
continue
if xml_parser.get_node_type() == XMLParser.NODE_ELEMENT and xml_parser.is_empty() and xml_parser.get_node_name() in ["defs", "g", "clipPath"]:
continue
if xml_parser.get_node_type() == XMLParser.NODE_ELEMENT_END:
if svg_xml_node.parent:
svg_xml_node = svg_xml_node.parent
else:
var new_svg_xml_node := SVGXMLElement.new(xml_parser, svg_xml_node)
if svg_xml_node:
svg_xml_node.add_child(new_svg_xml_node)
if not xml_parser.is_empty():
svg_xml_node = new_svg_xml_node
return svg_xml_node
func process_svg_xml_tree(xml_data : SVGXMLElement, scene_root : Node, svg_root :
Node2D, current_node : Node2D, svg_gradients : Array[Dictionary]) -> void:
if xml_data.name == "use":
var href = xml_data.get_named_attribute_value_safe("xlink:href")
if href.is_empty():
href = xml_data.get_named_attribute_value_safe("href")
var reuse_xml_node = xml_data.find_by_id(href.replace("#", ""))
var style = xml_data.get_svg_style(log_message)
style.merge(reuse_xml_node.get_merged_styles(log_message))
var preserve_id := xml_data.get_named_attribute_value_safe("id")
xml_data.attributes.erase("xlink:href")
xml_data.attributes.merge(reuse_xml_node.attributes)
xml_data.attributes["id"] = preserve_id
xml_data.attributes["style"] = "; ".join(style.keys().map(func(k): return k + ": " + style[k]))
if preserve_id.is_empty():
xml_data.attributes.erase("id")
xml_data.name = reuse_xml_node.name
match xml_data.name:
"svg":
if xml_data.has_attribute("viewBox") and xml_data.has_attribute("width") and xml_data.has_attribute("height"):
var view_box = xml_data.get_named_attribute_value("viewBox").split_floats(" ")
var width := float(xml_data.get_named_attribute_value("width"))
var height := float(xml_data.get_named_attribute_value("height"))
svg_root.scale.x = width / view_box[2]
svg_root.scale.y = height / view_box[3]
if xml_data.get_named_attribute_value("width").ends_with("mm"): # unit conversion to pixel
log_message("⚠️ Units for this image are millimeters (mm), image scale set to 3.78")
svg_root.scale *= 3.78
elif xml_data.get_named_attribute_value("width").ends_with("cm"):
log_message("⚠️ Units for this image are centimeters (cm), image scale set to 37.8")
svg_root.scale *= 37.8
if xml_data.has_attribute("style"):
current_node.set_meta(SVG_STYLE_META_NAME, xml_data.get_merged_styles(log_message))
"g":
current_node = process_group(xml_data, current_node, scene_root)
"clipPath", "defs":
current_node = process_group(xml_data, current_node, scene_root, xml_data.name)
current_node.hide()
"rect":
process_svg_rectangle(xml_data, current_node, scene_root, svg_gradients)
"image":
process_svg_image(xml_data, current_node, scene_root, svg_gradients)
"polygon":
process_svg_polygon(xml_data, current_node, scene_root, true, svg_gradients)
"polyline":
process_svg_polygon(xml_data, current_node, scene_root, false, svg_gradients)
"path":
process_svg_path(xml_data, current_node, scene_root, svg_gradients)
"circle":
process_svg_circle(xml_data, current_node, scene_root, svg_gradients)
"ellipse":
process_svg_ellipse(xml_data, current_node, scene_root, svg_gradients)
"linearGradient", "radialGradient":
svg_gradients.append(parse_gradient(xml_data))
"stop":
pass
_: log_message("⚠️ Skipping unsupported node: <%s>" % xml_data.name, LogLevel.DEBUG)
var defs := xml_data.children.filter(func(ch): return ch.name == "defs")
var clip_paths := xml_data.children.filter(func(ch): return ch.name == "clipPath")
var remainder := xml_data.children.filter(func(ch): return ch.name != "defs" and ch.name != "clipPath")
for ch in defs + clip_paths + remainder:
process_svg_xml_tree(ch, scene_root, svg_root, current_node, svg_gradients)
func get_gradient_by_href(href : String, gradients : Array[Dictionary]) -> Dictionary:
var idx := gradients.find_custom(func(x): return "id" in x and "#" + x["id"] == href)
if idx < 0:
return {}
return gradients[idx]
func parse_gradient(gradient_xml : SVGXMLElement) -> Dictionary:
var new_gradient = {
'is_radial': gradient_xml.get_node_name() == "radialGradient"
}
for x in gradient_xml.attributes:
new_gradient[x] = gradient_xml.attributes[x]
if not gradient_xml.is_empty():
new_gradient["stops"] = []
for element in gradient_xml.children:
if element.get_node_name() == "stop":
new_gradient["stops"].append({
"style": element.get_merged_styles(log_message),
"offset": float(element.get_named_attribute_value_safe("offset")),
"id": element.get_named_attribute_value_safe("id")
})
return new_gradient
func process_group(element:SVGXMLElement, current_node : Node2D, scene_root : Node, alt_name := "Group") -> Node2D:
var new_group = Node2D.new()
new_group.name = element.get_named_attribute_value("id") if element.has_attribute("id") else alt_name
new_group.transform = get_svg_transform(element)
var style := element.get_merged_styles(log_message)
new_group.set_meta(SVG_STYLE_META_NAME, style)
if style.has("display") and style['display'] == "none":
new_group.visible = false
_managed_add_child_and_set_owner(current_node, new_group, scene_root)
return new_group
func process_svg_circle(element:SVGXMLElement, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
var cx = float(element.get_named_attribute_value("cx"))
var cy = float(element.get_named_attribute_value("cy"))
var r = float(element.get_named_attribute_value("r"))
var path_name = element.get_named_attribute_value("id") if element.has_attribute("id") else "Circle"
create_path_from_ellipse(element, path_name, r, r, Vector2(cx, cy), current_node, scene_root, gradients)
func process_svg_ellipse(element:SVGXMLElement, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
var cx = float(element.get_named_attribute_value("cx"))
var cy = float(element.get_named_attribute_value("cy"))
var rx = float(element.get_named_attribute_value("rx"))
var ry = float(element.get_named_attribute_value("ry"))
var path_name = element.get_named_attribute_value("id") if element.has_attribute("id") else "Ellipse"
create_path_from_ellipse(element, path_name, rx, ry, Vector2(cx, cy), current_node, scene_root, gradients)
func create_path_from_ellipse(element:SVGXMLElement, path_name : String, rx : float, ry: float,
pos : Vector2, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
var new_ellipse := ScalableVectorShape2D.new()
new_ellipse.shape_type = ScalableVectorShape2D.ShapeType.ELLIPSE
new_ellipse.size = Vector2(rx * 2, ry * 2)
new_ellipse.position = pos
new_ellipse.name = path_name
_post_process_shape(new_ellipse, current_node, get_svg_transform(element),
element.get_merged_styles(log_message), scene_root, gradients)
func process_svg_image(element:SVGXMLElement, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
var x = float(element.get_named_attribute_value("x")) if element.has_attribute("x") else 0.0
var y = float(element.get_named_attribute_value("y")) if element.has_attribute("y") else 0.0
var width = float(element.get_named_attribute_value("width"))
var height = float(element.get_named_attribute_value("height"))
var new_image_rect := ScalableVectorShape2D.new()
new_image_rect.shape_type = ScalableVectorShape2D.ShapeType.RECT
new_image_rect.size = Vector2(width, height)
new_image_rect.position = Vector2(x, y) + new_image_rect.size * 0.5
new_image_rect.name = element.get_named_attribute_value("id") if element.has_attribute("id") else "Image"
var image_data : String = (
element.get_named_attribute_value("xlink:href")
if element.has_attribute("xlink:href") else
element.get_named_attribute_value_safe("href")
)
var image_texture : ImageTexture = null
if image_data.begins_with("data:image") and image_data.contains("base64"):
var parts_a := image_data.split(",")
var parts_b := parts_a[0].split("/")
var format := parts_b[1].replace(";", "").replace("base64", "").strip_edges()
var base_64_data := parts_a[1].strip_edges()
var unmarshalled := Marshalls.base64_to_raw(base_64_data)
var image := Image.new()
image.call("load_%s_from_buffer" % format.to_lower(), unmarshalled)
image_texture = ImageTexture.create_from_image(image)
log_message("Parsed image format: %s" % format, LogLevel.DEBUG)
else:
log_message("⚠️ Only base64 encoded embedded images are supported", LogLevel.WARN)
_post_process_shape(new_image_rect, current_node, get_svg_transform(element),
element.get_merged_styles(log_message), scene_root, gradients, false, image_texture)
func process_svg_rectangle(element:SVGXMLElement, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
var x = float(element.get_named_attribute_value("x"))
var y = float(element.get_named_attribute_value("y"))
var rx = float(element.get_named_attribute_value("rx")) if element.has_attribute("rx") else 0
var ry = float(element.get_named_attribute_value("ry")) if element.has_attribute("ry") else 0
if rx == 0 and ry != 0:
rx = ry
if ry == 0 and rx != 0:
ry = rx
var width = float(element.get_named_attribute_value("width"))
var height = float(element.get_named_attribute_value("height"))
var new_rect := ScalableVectorShape2D.new()
new_rect.shape_type = ScalableVectorShape2D.ShapeType.RECT
new_rect.size = Vector2(width, height)
new_rect.position = Vector2(x, y) + new_rect.size * 0.5
new_rect.rx = rx
new_rect.ry = ry
new_rect.name = element.get_named_attribute_value("id") if element.has_attribute("id") else "Rectangle"
_post_process_shape(new_rect, current_node, get_svg_transform(element),
element.get_merged_styles(log_message), scene_root, gradients)
func process_svg_polygon(element:SVGXMLElement, current_node : Node2D, scene_root : Node, is_closed : bool,
gradients : Array[Dictionary]) -> void:
var points_split = element.get_named_attribute_value("points").split(" ", false)
var curve = Curve2D.new()
for p in points_split:
var values = p.split_floats(",", false)
curve.add_point(Vector2(values[0], values[1]))
var path_name = (element.get_named_attribute_value("id") if element.has_attribute("id") else
"Polygon" if is_closed else
"Polyline"
)
create_path2d(path_name, current_node, curve, [], get_svg_transform(element),
element.get_merged_styles(log_message), scene_root, gradients, is_closed)
func process_svg_path(element:SVGXMLElement, current_node : Node2D, scene_root : Node,
gradients : Array[Dictionary]) -> void:
# FIXME: implement better parsing here
var str_path = parse_attribute_string(
element.get_named_attribute_value("d")).replacen(",", " ")
var shape_name := element.get_named_attribute_value("id") if element.has_attribute("id") else "Path"
for symbol in ["m", "M", "v", "V", "h", "H", "l", "L", "c", "C", "s", "S", "a", "A", "q", "Q", "t", "T", "z", "Z"]:
str_path = str_path.replace(symbol, " " + symbol + " ")
# FIXME: this bit is especially problematic
str_path = str_path.replace("e-", PLC_EXP)
str_path = str_path.replace("-", " -")
str_path = str_path.replace(PLC_EXP, "e-")
var str_path_array = str_path.split(" ", false)
var string_arrays = []
var string_array_top : PackedStringArray
for a in str_path_array:
if a == "m" or a == "M":
if string_array_top.size() > 0:
string_arrays.append(string_array_top.duplicate())
string_array_top.resize(0)
string_array_top.append(a)
string_arrays.append(string_array_top)
if string_arrays.size() > 1:
log_message("⚠️ Support for the m/M (move to) command is limited to cut-outs in svg paths")
var string_array_count = 0
var cursor = Vector2.ZERO
var shapes : Array[ScalableVectorShape2D] = []
for string_array in string_arrays:
var curve = Curve2D.new()
var arcs : Array[ScalableArc] = []
string_array_count += 1
var cursor_start := Vector2.ZERO
for i in string_array.size():
var cursor_start_is_set := false
match string_array[i]:
"m":
while string_array.size() > i + 2 and string_array[i+1].is_valid_float():
cursor += Vector2(float(string_array[i+1]), float(string_array[i+2]))
curve.add_point(cursor)
i += 2
if not cursor_start_is_set:
cursor_start_is_set = true
cursor_start = cursor
"M":
while string_array.size() > i + 2 and string_array[i+1].is_valid_float():
cursor = Vector2(float(string_array[i+1]), float(string_array[i+2]))
curve.add_point(cursor)
i += 2
if not cursor_start_is_set:
cursor_start_is_set = true
cursor_start = cursor
"v":
while string_array[i+1].is_valid_float():
cursor.y += float(string_array[i+1])
curve.add_point(cursor)
i += 1
"V":
while string_array[i+1].is_valid_float():
cursor.y = float(string_array[i+1])
curve.add_point(cursor)
i += 1
"h":
while string_array[i+1].is_valid_float():
cursor.x += float(string_array[i+1])
curve.add_point(cursor)
i += 1
"H":
while string_array[i+1].is_valid_float():
cursor.x = float(string_array[i+1])
curve.add_point(cursor)
i += 1
"l":
while string_array.size() > i + 2 and string_array[i+1].is_valid_float():
cursor += Vector2(float(string_array[i+1]), float(string_array[i+2]))
curve.add_point(cursor)
i += 2
"L":
while string_array.size() > i + 2 and string_array[i+1].is_valid_float():
cursor = Vector2(float(string_array[i+1]), float(string_array[i+2]))
curve.add_point(cursor)
i += 2
"c":
while string_array.size() > i + 6 and string_array[i+1].is_valid_float():
var c_out := Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_2 := Vector2(float(string_array[i+3]), float(string_array[i+4]))
var c_in_absolute = cursor + c_2
curve.set_point_out(curve.get_point_count() - 1, c_out)
cursor += Vector2(float(string_array[i+5]), float(string_array[i+6]))
var c_in = c_in_absolute - cursor
curve.add_point(cursor)
curve.set_point_in(curve.get_point_count() - 1, c_in)
i += 6
"C":
while string_array.size() > i + 6 and string_array[i+1].is_valid_float():
var c_out := Vector2(float(string_array[i+1]), float(string_array[i+2]))
var prev_point := curve.get_point_position(curve.get_point_count() - 1)
var c_in := Vector2(float(string_array[i+3]), float(string_array[i+4]))
curve.set_point_out(curve.get_point_count() - 1, c_out - prev_point)
cursor = Vector2(float(string_array[i+5]), float(string_array[i+6]))
curve.add_point(cursor, c_in - cursor)
i += 6
"s":
while string_array.size() > i + 4 and string_array[i+1].is_valid_float():
var c_out := -curve.get_point_in(curve.get_point_count() - 1)
var c_2 := Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_in_absolute = cursor + c_2
curve.set_point_out(curve.get_point_count() - 1, c_out)
cursor += Vector2(float(string_array[i+3]), float(string_array[i+4]))
var c_in = c_in_absolute - cursor
curve.add_point(cursor)
curve.set_point_in(curve.get_point_count() - 1, c_in)
i += 4
"S":
while string_array.size() > i + 4 and string_array[i+1].is_valid_float():
var c_out := -curve.get_point_in(curve.get_point_count() - 1)
curve.set_point_out(curve.get_point_count() - 1, c_out)
cursor = Vector2(float(string_array[i+3]), float(string_array[i+4]))
var c_in := Vector2(float(string_array[i+1]), float(string_array[i+2]))
curve.add_point(cursor, c_in - cursor)
i += 4
"q":
while string_array.size() > i + 4 and string_array[i+1].is_valid_float():
var prev_point := curve.get_point_position(curve.get_point_count() - 1)
var quadratic_control_point = cursor + Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_out = (quadratic_control_point - prev_point) * (2.0/3.0)
cursor += Vector2(float(string_array[i+3]), float(string_array[i+4]))
var c_in = (quadratic_control_point - cursor) * (2.0/3.0)
curve.set_point_out(curve.get_point_count() - 1, c_out)
curve.add_point(cursor, c_in)
i += 4
"Q":
while string_array.size() > i + 4 and string_array[i+1].is_valid_float():
var prev_point := curve.get_point_position(curve.get_point_count() - 1)
var quadratic_control_point := Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_out = (quadratic_control_point - prev_point) * (2.0/3.0)
cursor = Vector2(float(string_array[i+3]), float(string_array[i+4]))
var c_in = (quadratic_control_point - cursor) * (2.0/3.0)
curve.set_point_out(curve.get_point_count() - 1, c_out)
curve.add_point(cursor, c_in)
i += 4
"t":
while string_array.size() > i + 2 and string_array[i+2].is_valid_float():
var c_out := -curve.get_point_in(curve.get_point_count() - 1)
var quadratic_control_point := curve.get_point_position(curve.get_point_count() - 1) + (c_out / (2.0/3.0))
curve.set_point_out(curve.get_point_count() - 1, c_out)
cursor += Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_in = (quadratic_control_point - cursor) * (2.0/3.0)
curve.add_point(cursor, c_in)
i += 2
"T":
while string_array.size() > i + 2 and string_array[i+2].is_valid_float():
var c_out := -curve.get_point_in(curve.get_point_count() - 1)
var quadratic_control_point := curve.get_point_position(curve.get_point_count() - 1) + (c_out / (2.0/3.0))
curve.set_point_out(curve.get_point_count() - 1, c_out)
cursor = Vector2(float(string_array[i+1]), float(string_array[i+2]))
var c_in = (quadratic_control_point - cursor) * (2.0/3.0)
curve.add_point(cursor, c_in)
i += 2
"a":
while string_array.size() > i + 7 and string_array[i+1].is_valid_float():
arcs.append(ScalableArc.new(
curve.point_count - 1,
Vector2(float(string_array[i+1]), float(string_array[i+2])),
float(string_array[i+3]),
int(string_array[i+4]) == 1,
int(string_array[i+5]) == 1
))
cursor += Vector2(float(string_array[i+6]), float(string_array[i+7]))
curve.add_point(cursor)
i += 7
"A":
while string_array.size() > i + 7 and string_array[i+1].is_valid_float():
arcs.append(ScalableArc.new(
curve.point_count - 1,
Vector2(float(string_array[i+1]), float(string_array[i+2])),
float(string_array[i+3]),
int(string_array[i+4]) == 1,
int(string_array[i+5]) == 1
))
cursor = Vector2(float(string_array[i+6]), float(string_array[i+7]))
curve.add_point(cursor)
i += 7
"z", "Z":
cursor = cursor_start
# Add a new ScalableVectorShape2D to the list for this section of
# the path definition (`d`-attribute of the path element)
var shape := ScalableVectorShape2D.new()
shape.name = shape_name
shape.curve = curve
shape.arc_list = ScalableArcList.new(arcs)
shape.set_meta("is_closed", string_array[string_array.size()-1].to_upper() == "Z")
shapes.append(shape)
log_message("Postprocessing for %s" % shape_name, LogLevel.DEBUG)
# Loop through al the shapes in this <path> element looking for holes
# if a shape is a hole, make sure it is not in the post_processed_shapes
# array after this loop, but a member of the surrounding shape's clip_paths
# array.
var post_processed_shapes : Array[ScalableVectorShape2D] = []
for shape : ScalableVectorShape2D in shapes:
var poly := shape.tessellate()
post_processed_shapes.append(shape)
for shape1 : ScalableVectorShape2D in shapes:
if shape1 == shape:
continue
var poly1 := shape1.tessellate()
var res := Geometry2D.intersect_polygons(poly, poly1)
if res.size() > 0:
if Geometry2D.is_point_in_polygon(poly[0], poly1):
if shape not in shape1.clip_paths:
shape1.clip_paths.append(shape)
post_processed_shapes.erase(shape)
else:
if shape1 not in shape.clip_paths:
shape.clip_paths.append(shape1)
post_processed_shapes.erase(shape1)
# Append actual new shapes to the scene by copying the `curve`, `arc_list` and
# `clip_paths`. Also, the shapes inside the `clip_paths` property are added as
# actual node in the resulting scene
for shape in post_processed_shapes:
var new_path := create_path2d(shape_name, current_node, shape.curve.duplicate(true), shape.arc_list.arcs.duplicate(true), get_svg_transform(element),
element.get_merged_styles(log_message), scene_root, gradients, shape.get_meta("is_closed"))
var clips : Array[ScalableVectorShape2D] = []
for cutout in shape.clip_paths:
clips.append(create_path2d("CutoutFor%s" % shape_name, current_node, cutout.curve.duplicate(true), cutout.arc_list.arcs.duplicate(true),
Transform2D.IDENTITY, {}, scene_root, gradients, cutout.get_meta("is_closed"), new_path))
cutout.free()
shape.free()
# append_array is used here, because clip paths may already have been added via the
# `create_path2d(...)` call chain.
new_path.clip_paths.append_array(clips)
undo_redo.add_do_property(new_path, 'clip_paths', new_path.clip_paths)
undo_redo.add_undo_property(new_path, 'clip_paths', [])
func create_path2d(path_name: String, parent: Node, curve: Curve2D, arcs: Array[ScalableArc],
transform: Transform2D, style: Dictionary, scene_root: Node,
gradients : Array[Dictionary], is_closed := false,
is_cutout_for : ScalableVectorShape2D = null) -> ScalableVectorShape2D:
var new_path = ScalableVectorShape2D.new()
new_path.name = path_name
new_path.curve = curve
new_path.arc_list = ScalableArcList.new(arcs)
if (is_closed and curve.point_count > 1 and curve.get_point_position(0).distance_to(
curve.get_point_position(curve.point_count - 1)) > 0.001):
curve.add_point(curve.get_point_position(0))
if is_cutout_for:
new_path.transform = is_cutout_for.transform.affine_inverse()
new_path.set_position_to_center()
_post_process_shape(new_path, is_cutout_for, transform, style, scene_root, gradients, true)
else:
new_path.set_position_to_center()
_post_process_shape(new_path, parent, transform, style, scene_root, gradients, false)
return new_path
func _apply_clip_path_by_href(href : String, svs : ScalableVectorShape2D, scene_root : Node):
var clip_path_node := scene_root.find_child(href.replace("url(#", "").replace(")", ""))
var new_clip_paths : Array[ScalableVectorShape2D] = []
for clip_path : ScalableVectorShape2D in clip_path_node.find_children("*", "ScalableVectorShape2D"):
clip_path.use_interect_when_clipping = true
if clip_path.line:
clip_path.line.hide()
if clip_path.polygon:
clip_path.polygon.hide()
var applied_clip_path = clip_path.duplicate()
new_clip_paths.append(applied_clip_path)
_managed_add_child_and_set_owner(svs.get_parent(), applied_clip_path, scene_root)
log_message("Processing %d clip-paths for %s" % [new_clip_paths.size(), svs.name], LogLevel.DEBUG)
svs.clip_paths = new_clip_paths
undo_redo.add_do_property(svs, 'clip_paths', new_clip_paths)
undo_redo.add_undo_property(svs, 'clip_paths', [])
func _post_process_shape(svs : ScalableVectorShape2D, parent : Node, transform : Transform2D,
style : Dictionary, scene_root : Node, gradients : Array[Dictionary],
is_cutout := false, image_texture : ImageTexture = null) -> void:
svs.lock_assigned_shapes = import_as_svs and lock_shapes
svs.update_curve_at_runtime = CurvedLines2D._is_setting_update_curve_at_runtime()
svs.arc_list.resource_local_to_scene = CurvedLines2D._is_making_curve_resources_local_to_scene()
svs.curve.resource_local_to_scene = CurvedLines2D._is_making_curve_resources_local_to_scene()
svs.tolerance_degrees = CurvedLines2D._get_default_tolerance_degrees()
svs.max_stages = CurvedLines2D._get_default_max_stages()
var gradient_point_parent : Node2D = parent
if transform == Transform2D.IDENTITY:
_managed_add_child_and_set_owner(parent, svs, scene_root)
else:
var transform_node := Node2D.new()
transform_node.name = svs.name + "Transform"
transform_node.transform = transform
_managed_add_child_and_set_owner(parent, transform_node, scene_root)
_managed_add_child_and_set_owner(transform_node, svs, scene_root)
gradient_point_parent = transform_node
if style.has("opacity"):
svs.modulate.a = float(style["opacity"])
if style.is_empty() or ("fill" not in style and "stroke" not in style):
style["fill"] = "#000000"
if style.has("display") and style['display'] == "none":
svs.visible = false
if not is_cutout:
for func_name in PAINT_ORDER_MAP[get_paint_order(style)]:
call(func_name, svs, style, scene_root, gradients,
gradient_point_parent, image_texture)
if "clip-path" in style:
_apply_clip_path_by_href(style["clip-path"], svs, scene_root)
func get_paint_order(style : Dictionary) -> String:
if style.has("paint-order") and style['paint-order'] in PAINT_ORDER_MAP:
return style['paint-order']
else:
return "normal"
func add_stroke_to_path(new_path : ScalableVectorShape2D, style: Dictionary, scene_root : Node,
gradients : Array[Dictionary], gradient_point_parent : Node2D,
_image_texture : ImageTexture):
if style.has("stroke") and style["stroke"] != "none":
var stroke : Node2D = Line2D.new() if import_stroke_as_line_2d else Polygon2D.new()
var prop_name := "line" if import_stroke_as_line_2d else "poly_stroke"
stroke.name = "Stroke"
stroke.antialiased = antialiased_shapes
_managed_add_child_and_set_owner(new_path, stroke, scene_root, prop_name)
if style["stroke"].begins_with("url"):
if stroke is Line2D:
log_message("⚠️ Gradient stroke style not supported by Line2D: " + style["stroke"])
else:
var href : String = style["stroke"].replace("url(", "").replace(")", "")
var svg_gradient = get_gradient_by_href(href, gradients)
if svg_gradient.is_empty():
log_message("⚠️ Cannot find gradient for href=%s" % href, LogLevel.WARN)
else:
add_gradient_to_fill(new_path, svg_gradient, stroke, scene_root, gradients, gradient_point_parent)
elif style["stroke"].begins_with("rgba"):
var parts := _parse_svg_transform_params(style["stroke"].replace("rgba", ""))
new_path.stroke_color = Color.from_rgba8(parts[0], parts[1], parts[2], parts[3])
elif style["stroke"].begins_with("rgb"):
var parts := _parse_svg_transform_params(style["stroke"].replace("rgb", ""))
new_path.stroke_color = Color.from_rgba8(parts[0], parts[1], parts[2])
else:
new_path.stroke_color = Color(style["stroke"])
if style.has("stroke-width"):
new_path.stroke_width = float(style['stroke-width'])
if style.has("stroke-opacity"):
new_path.stroke_color.a = float(style["stroke-opacity"])
if style.has("stroke-linecap") and style["stroke-linecap"] in STROKE_CAP_MAP:
new_path.end_cap_mode = STROKE_CAP_MAP[style["stroke-linecap"]]
new_path.begin_cap_mode = STROKE_CAP_MAP[style["stroke-linecap"]]
else:
new_path.end_cap_mode = Line2D.LINE_CAP_NONE
new_path.begin_cap_mode = Line2D.LINE_CAP_NONE
if style.has("stroke-linejoin") and style["stroke-linejoin"] in STROKE_JOINT_MAP:
new_path.line_joint_mode = STROKE_JOINT_MAP[style["stroke-linejoin"]]
else:
new_path.line_joint_mode = Line2D.LINE_JOINT_SHARP
if stroke is Line2D:
if style.has("stroke-miterlimit"):
stroke.sharp_limit = float(style["stroke-miterlimit"])
else:
stroke.sharp_limit = 4.0 # svg default
if CurvedLines2D._use_antialiased_line_2d():
stroke.texture = load("res://addons/curved_lines_2d/LumAlpha8.tex")
stroke.texture_mode = Line2D.LINE_TEXTURE_TILE
stroke.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
func add_fill_to_path(new_path : ScalableVectorShape2D, style: Dictionary, scene_root : Node,
gradients : Array[Dictionary], gradient_point_parent : Node2D,
image_texture : ImageTexture):
if image_texture or style.has("fill") and style["fill"] != "none":
var polygon := Polygon2D.new()
polygon.name = "Fill"
polygon.antialiased = antialiased_shapes
_managed_add_child_and_set_owner(new_path, polygon, scene_root, 'polygon')
if image_texture != null:
var box := new_path.get_bounding_rect()
polygon.texture = image_texture
polygon.texture_offset = -box.position
polygon.texture_scale = polygon.texture.get_size() / box.size
elif style["fill"].begins_with("url"):
var href : String = style["fill"].replace("url(", "").replace(")", "")
var svg_gradient = get_gradient_by_href(href, gradients)
if svg_gradient.is_empty():
log_message("⚠️ Cannot find gradient for href=%s" % href, LogLevel.WARN)
else:
add_gradient_to_fill(new_path, svg_gradient, polygon, scene_root, gradients, gradient_point_parent)
elif style["fill"].begins_with("rgba"):
var parts := _parse_svg_transform_params(style["fill"].replace("rgba", ""))
polygon.color = Color.from_rgba8(parts[0], parts[1], parts[2], parts[3])
elif style["fill"].begins_with("rgb"):
var parts := _parse_svg_transform_params(style["fill"].replace("rgb", ""))
polygon.color = Color.from_rgba8(parts[0], parts[1], parts[2])
else:
polygon.color = Color(style["fill"])
if style.has("fill-opacity"):
polygon.color.a = float(style["fill-opacity"])
func add_collision_to_path(new_path : ScalableVectorShape2D, style : Dictionary, scene_root : Node,
_gradients : Array[Dictionary], _gradient_point_parent : Node2D,
_image_texture : ImageTexture) -> void:
if collision_object_type != ScalableVectorShape2D.CollisionObjectType.NONE:
match collision_object_type:
ScalableVectorShape2D.CollisionObjectType.STATIC_BODY_2D:
_managed_add_child_and_set_owner(new_path, StaticBody2D.new(), scene_root, 'collision_object')
ScalableVectorShape2D.CollisionObjectType.AREA_2D:
_managed_add_child_and_set_owner(new_path, Area2D.new(), scene_root, 'collision_object')
ScalableVectorShape2D.CollisionObjectType.ANIMATABLE_BODY_2D:
_managed_add_child_and_set_owner(new_path, AnimatableBody2D.new(), scene_root, 'collision_object')
ScalableVectorShape2D.CollisionObjectType.RIGID_BODY_2D:
_managed_add_child_and_set_owner(new_path, RigidBody2D.new(), scene_root, 'collision_object')
ScalableVectorShape2D.CollisionObjectType.CHARACTER_BODY_2D:
_managed_add_child_and_set_owner(new_path, CharacterBody2D.new(), scene_root, 'collision_object')
ScalableVectorShape2D.CollisionObjectType.PHYSICAL_BONE_2D:
_managed_add_child_and_set_owner(new_path, PhysicalBone2D.new(), scene_root, 'collision_object')
func add_gradient_to_fill(new_path : ScalableVectorShape2D, svg_gradient: Dictionary, polygon : Polygon2D,
scene_root : Node, gradients : Array[Dictionary], gradient_point_parent : Node2D) -> void:
if "xlink:href" in svg_gradient:
svg_gradient.merge(get_gradient_by_href(svg_gradient["xlink:href"], gradients), false)
elif "href" in svg_gradient:
svg_gradient.merge(get_gradient_by_href(svg_gradient["href"], gradients), false)
var texture := GradientTexture2D.new()
var box := new_path.get_bounding_rect()
texture.width = ceil(box.size.x)
texture.height = ceil(box.size.y)
texture.gradient = Gradient.new()
var stops = svg_gradient["stops"] if "stops" in svg_gradient else []
var gradient_data := {}
for i in range(stops.size()):
var stop_style = stops[i]["style"] if "style" in stops[i] else { "stop-color": "#ffffff" }
var stop_color = stop_style["stop-color"] if "stop-color" in stop_style else "#ffffff"
var stop_opacity = stop_style["stop-opacity"] if "stop-opacity" in stop_style else "1"
gradient_data[float(stops[i]["offset"])] = Color(stop_color, float(stop_opacity))
texture.gradient.colors = gradient_data.values()
texture.gradient.offsets = gradient_data.keys()
if svg_gradient["is_radial"] and "cx" in svg_gradient and "cy" in svg_gradient and "r" in svg_gradient:
var gradient_transform = (
process_svg_transform(svg_gradient["gradientTransform"]) if "gradientTransform" in svg_gradient else
Transform2D.IDENTITY
)
var fill_from = Vector2(float(svg_gradient["cx"]), float(svg_gradient["cy"]))
var fill_to = fill_from + Vector2.RIGHT * float(svg_gradient["r"])
apply_gradient(new_path, svg_gradient, polygon, scene_root, gradients, gradient_point_parent,
box, texture, fill_from, fill_to, gradient_transform)
texture.fill = GradientTexture2D.FILL_RADIAL
elif "x1" in svg_gradient and "y1" in svg_gradient and "x2" in svg_gradient and "y2" in svg_gradient:
var gradient_transform = (
process_svg_transform(svg_gradient["gradientTransform"]) if "gradientTransform" in svg_gradient else
Transform2D.IDENTITY
)
var fill_from = Vector2(float(svg_gradient["x1"]), float(svg_gradient["y1"]))
var fill_to = Vector2(float(svg_gradient["x2"]), float(svg_gradient["y2"]))
apply_gradient(new_path, svg_gradient, polygon, scene_root, gradients, gradient_point_parent,
box, texture, fill_from, fill_to, gradient_transform)
polygon.texture_offset = -box.position
polygon.texture = texture
func apply_gradient(new_path : ScalableVectorShape2D, svg_gradient: Dictionary, polygon : Polygon2D,
scene_root : Node, gradients : Array[Dictionary], gradient_point_parent : Node2D, box : Rect2,
texture : GradientTexture2D, fill_from : Vector2, fill_to : Vector2, gradient_transform : Transform2D) -> void:
var gradient_transform_node = create_helper_node("Gradient(%s)" % new_path.name, gradient_point_parent, scene_root, Vector2.ZERO, gradient_transform)
var from_node = create_helper_node("From(%s)" % new_path.name, gradient_transform_node, scene_root, fill_from)
var to_node = create_helper_node("To(%s)" % new_path.name, gradient_transform_node, scene_root, fill_to)
var box_tl_node = create_helper_node("BoxTopLeft(%s)" % new_path.name, gradient_point_parent, scene_root, new_path.position + box.position)
var box_br_node = create_helper_node("BoxBottomRight(%s)" % new_path.name, gradient_point_parent, scene_root, box_tl_node.position + box.size)
texture.fill_from = (from_node.global_position - box_tl_node.global_position) / (box_br_node.global_position - box_tl_node.global_position)
texture.fill_to = (to_node.global_position - box_tl_node.global_position) / (box_br_node.global_position - box_tl_node.global_position)
gradient_transform_node.queue_free()
box_tl_node.queue_free()
box_br_node.queue_free()
func create_helper_node(node_name : String, node_parent : Node2D, node_owner : Node,
node_position := Vector2.ZERO, node_transform := Transform2D.IDENTITY) -> Node2D:
var helper_node := Node2D.new()
helper_node.name = node_name
node_parent.add_child(helper_node, true)
helper_node.set_owner(node_owner)
if node_position != Vector2.ZERO:
helper_node.position = node_position
if node_transform != Transform2D.IDENTITY:
helper_node.transform = node_transform
return helper_node
func get_svg_transform(element:SVGXMLElement) -> Transform2D:
if element.has_attribute("transform"):
return process_svg_transform(element.get_named_attribute_value("transform"))
else:
return Transform2D.IDENTITY
func _parse_svg_transform_params(svg_transform_params : String) -> PackedFloat64Array:
return (svg_transform_params
.replace("(", "").replace(")", "").replace(",", " ")
.split_floats(" ", false))
func process_svg_transform(svg_transform_attr : String) -> Transform2D:
var svg_commands = (
Array(svg_transform_attr.split(")", false))
.map(func(cmd): return cmd.lstrip(" \t\r\n") + ")")
)
svg_commands.reverse()
var transform = Transform2D.IDENTITY
for svg_transform in svg_commands:
if svg_transform.begins_with("translate"):
svg_transform = svg_transform.replace("translate", "")
var transform_split = _parse_svg_transform_params(svg_transform)
if transform_split.size() >= 2:
transform = transform.translated(Vector2(transform_split[0], transform_split[1]))
else:
transform = transform.translated(Vector2(transform_split[0], 0))
elif svg_transform.begins_with("scale"):
svg_transform = svg_transform.replace("scale", "")
var transform_split = _parse_svg_transform_params(svg_transform)
if transform_split.size() >= 2:
transform = transform.scaled(Vector2(transform_split[0], transform_split[1]))
else:
transform = transform.scaled(Vector2(transform_split[0], transform_split[0]))
elif svg_transform.begins_with("rotate"):
svg_transform = svg_transform.replace("rotate", "")
var transform_split = _parse_svg_transform_params(svg_transform)
if transform_split.size() == 1:
transform = transform.rotated(deg_to_rad(transform_split[0]))
elif transform_split.size() == 3:
transform = transform.translated(-Vector2(transform_split[1], transform_split[2]))
transform = transform.rotated(deg_to_rad(transform_split[0]))
transform = transform.translated(Vector2(transform_split[1], transform_split[2]))
elif svg_transform.begins_with("matrix"):
svg_transform = svg_transform.replace("matrix", "")
var matrix = _parse_svg_transform_params(svg_transform)
for i in 3:
transform[i] = Vector2(matrix[i*2], matrix[i*2+1])
return transform
func _managed_add_child_and_set_owner(parent : Node, child : Node,
scene_root : Node, as_property := ""):
parent.add_child(child, true)
child.set_owner(scene_root)
undo_redo.add_do_method(parent, 'add_child', child, true)
undo_redo.add_do_method(child, 'set_owner', scene_root)
undo_redo.add_do_reference(child)
undo_redo.add_undo_method(parent, 'remove_child', child)
if not as_property.is_empty():
parent.call("set", as_property, child)
undo_redo.add_do_property(parent, as_property, child)
static func parse_attribute_string(raw_attribute_str : String) -> String:
var regex = RegEx.new()
regex.compile("\\S+")
var str_path = ""
for result in regex.search_all(raw_attribute_str):
str_path += result.get_string() + " "
return str_path.strip_edges()
func _on_collision_object_type_option_button_type_selected(obj_type: ScalableVectorShape2D.CollisionObjectType) -> void:
collision_object_type = obj_type
func _on_keep_drawable_path_2d_node_check_box_toggled(toggled_on: bool) -> void:
import_as_svs = toggled_on
%LockShapesCheckBox.visible = toggled_on
func _on_lock_shapes_check_box_toggled(toggled_on: bool) -> void:
lock_shapes = toggled_on
func _on_antialiased_check_box_toggled(toggled_on: bool) -> void:
antialiased_shapes = toggled_on
func _on_open_file_dialog_button_pressed() -> void:
import_file_dialog.popup_file_dialog()
func _on_use_line_2d_check_box_toggled(toggled_on: bool) -> void:
import_stroke_as_line_2d = toggled_on