@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 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