diff --git a/assets/icons/Snap.svg b/assets/icons/Snap.svg
index 8edc101..4f39fe8 100644
--- a/assets/icons/Snap.svg
+++ b/assets/icons/Snap.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/backgrounds/CheckerboardMini.svg b/assets/icons/backgrounds/CheckerboardMini.svg
index 0f1f3d1..d185f8e 100644
--- a/assets/icons/backgrounds/CheckerboardMini.svg
+++ b/assets/icons/backgrounds/CheckerboardMini.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/element/circle.svg b/assets/icons/element/circle.svg
index f9edc29..0befcc7 100644
--- a/assets/icons/element/circle.svg
+++ b/assets/icons/element/circle.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/element/ellipse.svg b/assets/icons/element/ellipse.svg
index 1c4e677..17de190 100644
--- a/assets/icons/element/ellipse.svg
+++ b/assets/icons/element/ellipse.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/element/g.svg b/assets/icons/element/g.svg
index 9921ebb..344393c 100644
--- a/assets/icons/element/g.svg
+++ b/assets/icons/element/g.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/line.svg b/assets/icons/element/line.svg
index d9b5e2a..8f49bee 100644
--- a/assets/icons/element/line.svg
+++ b/assets/icons/element/line.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/element/linearGradient.svg b/assets/icons/element/linearGradient.svg
index 10491f3..058681e 100644
--- a/assets/icons/element/linearGradient.svg
+++ b/assets/icons/element/linearGradient.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/path.svg b/assets/icons/element/path.svg
index 35ade79..bf27000 100644
--- a/assets/icons/element/path.svg
+++ b/assets/icons/element/path.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/polygon.svg b/assets/icons/element/polygon.svg
index 8ef7302..334e157 100644
--- a/assets/icons/element/polygon.svg
+++ b/assets/icons/element/polygon.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/stop.svg b/assets/icons/element/stop.svg
index ac42d55..c490893 100644
--- a/assets/icons/element/stop.svg
+++ b/assets/icons/element/stop.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/svg.svg b/assets/icons/element/svg.svg
index 70eeeb9..ad5711a 100644
--- a/assets/icons/element/svg.svg
+++ b/assets/icons/element/svg.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/element/unrecognized.svg b/assets/icons/element/unrecognized.svg
index 97b8fb4..d020e43 100644
--- a/assets/icons/element/unrecognized.svg
+++ b/assets/icons/element/unrecognized.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/icons/element/use.svg b/assets/icons/element/use.svg
new file mode 100644
index 0000000..4301e10
--- /dev/null
+++ b/assets/icons/element/use.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/element/use.svg.import b/assets/icons/element/use.svg.import
new file mode 100644
index 0000000..cc0ca0a
--- /dev/null
+++ b/assets/icons/element/use.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1k88bqfhsvp5"
+path="res://.godot/imported/use.svg-7ae354a656abf9c2d947f0f7d986cf9a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/element/use.svg"
+dest_files=["res://.godot/imported/use.svg-7ae354a656abf9c2d947f0f7d986cf9a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/icons/foreign_logos/GithubLogo.svg b/assets/icons/foreign_logos/GithubLogo.svg
index 5d796c8..91ff8ff 100644
--- a/assets/icons/foreign_logos/GithubLogo.svg
+++ b/assets/icons/foreign_logos/GithubLogo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/foreign_logos/KoFiLogo.svg b/assets/icons/foreign_logos/KoFiLogo.svg
index 4d26af8..30b1d02 100644
--- a/assets/icons/foreign_logos/KoFiLogo.svg
+++ b/assets/icons/foreign_logos/KoFiLogo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/icons/theme/GuiToggleChecked.svg b/assets/icons/theme/GuiToggleChecked.svg
index 3a96e91..c3ed5f0 100644
--- a/assets/icons/theme/GuiToggleChecked.svg
+++ b/assets/icons/theme/GuiToggleChecked.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/godot_only/scripts/tests.gd b/godot_only/scripts/tests.gd
index 46e1148..38a96a2 100644
--- a/godot_only/scripts/tests.gd
+++ b/godot_only/scripts/tests.gd
@@ -5,12 +5,11 @@
extends EditorScript
func _run() -> void:
- var pathdata_test_passed := pathdata_tests()
- if pathdata_test_passed:
- print("All tests passed!")
+ pathdata_tests()
+ transform_list_tests()
-func pathdata_tests(print_success := false) -> bool:
+func pathdata_tests() -> void:
const tests: Dictionary[String, Array] = {
"Jerky": [],
"M 3s 6 h 6 v 3 z": [],
@@ -31,13 +30,34 @@ func pathdata_tests(print_success := false) -> bool:
"M1 6.9e-1": [["M", 1.0, 0.69]],
}
- var tests_passed := true
for test in tests:
var result := AttributePathdata.pathdata_to_arrays(test)
var expected: Array = tests[test]
if result != expected:
- tests_passed = false
print('"' + test + '" generated ' + str(result) + ', expected ' + str(expected))
- elif print_success:
- print('"' + test + '" generated ' + str(result) + ' (SUCCESS)')
- return tests_passed
+
+func transform_list_tests() -> void:
+ var tests: Dictionary[String, Array] = {
+ "Jerky": [],
+ "matrix(1, 0, 0, 5, 0, 3)": [Transform.TransformMatrix.new(1, 0, 0, 5, 0, 3)],
+ "matrix(1 0 0 5 0 3)": [Transform.TransformMatrix.new(1, 0, 0, 5, 0, 3)],
+ }
+
+ for test in tests:
+ var test_passed := true
+ var result := AttributeTransformList.text_to_transform_list(test)
+ var expected := tests[test]
+ if expected.size() != result.size():
+ test_passed = false
+ else:
+ for i in expected.size():
+ if expected[i] is Transform.TransformMatrix and\
+ (not result[i] is Transform.TransformMatrix or\
+ expected[i].x1 != result[i].x1 or expected[i].x2 != result[i].x2 or\
+ expected[i].y1 != result[i].y1 or expected[i].y2 != result[i].y2 or\
+ expected[i].o1 != result[i].o1 or expected[i].o2 != result[i].o2):
+ test_passed = false
+ break
+
+ if not test_passed:
+ print('"' + test + '" generated ' + str(result))
diff --git a/project.godot b/project.godot
index 4e74675..1669b9d 100644
--- a/project.godot
+++ b/project.godot
@@ -119,6 +119,14 @@ close_all_other_tabs={
"deadzone": 0.2,
"events": []
}
+close_empty_tabs={
+"deadzone": 0.2,
+"events": []
+}
+close_saved_tabs={
+"deadzone": 0.2,
+"events": []
+}
new_tab={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":84,"physical_keycode":0,"key_label":0,"unicode":116,"location":0,"echo":false,"script":null)
@@ -358,7 +366,7 @@ pen_tablet/driver.windows="dummy"
[internationalization]
rendering/root_node_auto_translate=false
-locale/translations=PackedStringArray("res://translations/bg.po", "res://translations/de.po", "res://translations/et.po", "res://translations/en.po", "res://translations/fr.po", "res://translations/nl.po", "res://translations/pt_BR.po", "res://translations/ru.po", "res://translations/uk.po", "res://translations/zh_CN.po")
+locale/translations=PackedStringArray("res://translations/bg.po", "res://translations/de.po", "res://translations/es.po", "res://translations/et.po", "res://translations/en.po", "res://translations/fr.po", "res://translations/nl.po", "res://translations/pt_BR.po", "res://translations/ru.po", "res://translations/uk.po", "res://translations/zh_CN.po")
[physics]
diff --git a/src/autoload/Configs.gd b/src/autoload/Configs.gd
index 072df73..edcfde7 100644
--- a/src/autoload/Configs.gd
+++ b/src/autoload/Configs.gd
@@ -66,9 +66,7 @@ func load_config() -> void:
reset_settings()
return
- savedata.get_active_tab().activate()
- change_background_color()
- change_locale()
+ post_load()
func reset_settings() -> void:
@@ -78,6 +76,12 @@ func reset_settings() -> void:
savedata.set_shortcut_panel_slots({ 0: "ui_undo", 1: "ui_redo", 2: "duplicate", 3: "save" })
savedata.set_palettes([Palette.new("Pure", Palette.Preset.PURE)])
save()
+ post_load()
+
+func post_load() -> void:
+ savedata.get_active_tab().activate()
+ sync_background_color()
+ sync_locale()
func generate_highlighter() -> SVGHighlighter:
@@ -95,10 +99,10 @@ func generate_highlighter() -> SVGHighlighter:
# Global effects from settings. Some of them should also be used on launch.
-func change_background_color() -> void:
+func sync_background_color() -> void:
RenderingServer.set_default_clear_color(savedata.background_color)
-func change_locale() -> void:
+func sync_locale() -> void:
if not savedata.language in TranslationServer.get_loaded_locales():
savedata.language = "en"
else:
diff --git a/src/autoload/HandlerGUI.gd b/src/autoload/HandlerGUI.gd
index 77c33a4..e25bc3e 100644
--- a/src/autoload/HandlerGUI.gd
+++ b/src/autoload/HandlerGUI.gd
@@ -43,7 +43,7 @@ func _notification(what: int) -> void:
# Drag-and-drop of files.
func _on_files_dropped(files: PackedStringArray) -> void:
if menu_stack.is_empty():
- FileUtils.apply_svg_from_path(files[0])
+ FileUtils.apply_svgs_from_paths(files)
func add_menu(new_menu: Control) -> void:
@@ -263,15 +263,21 @@ func _unhandled_input(event: InputEvent) -> void:
"save": FileUtils.save_svg()
"save_as": FileUtils.save_svg_as()
"close_tab": FileUtils.close_tabs(Configs.savedata.get_active_tab_index())
+ "close_all_other_tabs": FileUtils.close_tabs(
+ Configs.savedata.get_active_tab_index(),
+ FileUtils.TabCloseMode.ALL_OTHERS)
"close_tabs_to_left": FileUtils.close_tabs(
Configs.savedata.get_active_tab_index(),
FileUtils.TabCloseMode.TO_LEFT)
"close_tabs_to_right": FileUtils.close_tabs(
Configs.savedata.get_active_tab_index(),
FileUtils.TabCloseMode.TO_RIGHT)
- "close_all_other_tabs": FileUtils.close_tabs(
+ "close_empty_tabs": FileUtils.close_tabs(
Configs.savedata.get_active_tab_index(),
- FileUtils.TabCloseMode.ALL_OTHERS)
+ FileUtils.TabCloseMode.EMPTY)
+ "close_saved_tabs": FileUtils.close_tabs(
+ Configs.savedata.get_active_tab_index(),
+ FileUtils.TabCloseMode.SAVED)
"new_tab": Configs.savedata.add_empty_tab()
"select_next_tab": Configs.savedata.set_active_tab_index(
posmod(Configs.savedata.get_active_tab_index() + 1,
@@ -359,7 +365,7 @@ func get_max_ui_scale() -> float:
var window_default_size := get_window_default_size()
# How much can the default size be increased before it takes all usable screen space.
var max_expansion := Vector2(usable_screen_size) / Vector2(window_default_size)
- return clampf(snappedf(minf(max_expansion.x, max_expansion.y) - 0.025, 0.05), 0.75, 4.0)
+ return clampf(snappedf(minf(max_expansion.x, max_expansion.y) - 0.005, 0.01), 0.75, 4.0)
func get_min_ui_scale() -> float:
return maxf(snappedf(get_max_ui_scale() / 2.0 - 0.125, 0.25), 0.75)
@@ -519,7 +525,14 @@ func throw_mouse_motion_event() -> void:
var mm_event := InputEventMouseMotion.new()
var window := get_window()
# Must multiply by the final transform because the InputEvent is not yet parsed.
- mm_event.position = window.get_mouse_position() * window.get_final_transform()
+ var mouse_position = window.get_mouse_position()
+ # TODO This is a workaround because the returned mouse position is sometimes (0, 0),
+ # likely a Godot issue. This has been reproduced on Android and on Web.
+ # Reproducing on web is especially easy with zoom at something like 110% on Web.
+ if mouse_position == Vector2.ZERO:
+ return
+
+ mm_event.position = mouse_position * window.get_final_transform()
Input.parse_input_event.call_deferred(mm_event)
# Trigger a shortcut automatically.
diff --git a/src/autoload/State.gd b/src/autoload/State.gd
index 91903c3..3e36ddd 100644
--- a/src/autoload/State.gd
+++ b/src/autoload/State.gd
@@ -71,16 +71,9 @@ func _enter_tree() -> void:
cmdline_args.remove_at(0)
if cmdline_args.size() >= 1:
- # Need to wait a frame so the import warnings panel can be added.
+ # Need to wait a frame so the import warnings panel becomes available.
await get_tree().process_frame
-
- var used_tab_paths := PackedStringArray()
- for tab in Configs.savedata.get_tabs():
- used_tab_paths.append(tab.svg_file_path)
-
- for path in cmdline_args:
- if path.get_extension() == "svg" and not path in used_tab_paths:
- FileUtils.apply_svg_from_path(path)
+ FileUtils.apply_svgs_from_paths(cmdline_args)
func setup_from_tab() -> void:
var active_tab := Configs.savedata.get_active_tab()
@@ -91,7 +84,7 @@ func setup_from_tab() -> void:
return
if not new_text.is_empty():
- apply_svg_text(new_text)
+ apply_svg_text(new_text, false)
return
if active_tab.fully_loaded and not active_tab.empty_unsaved and\
diff --git a/src/config_classes/SaveData.gd b/src/config_classes/SaveData.gd
index 35db893..8ba9e8a 100644
--- a/src/config_classes/SaveData.gd
+++ b/src/config_classes/SaveData.gd
@@ -63,7 +63,7 @@ func validate() -> void:
if _layout.has(location) and not _layout[location].is_empty():
return
_layout = {
- LayoutLocation.TOP_LEFT: [Utils.LayoutPart.CODE_EDITOR, Utils.LayoutPart.INSPECTOR]
+ LayoutLocation.TOP_LEFT: [Utils.LayoutPart.INSPECTOR, Utils.LayoutPart.CODE_EDITOR]
}
@@ -79,7 +79,7 @@ const CURRENT_VERSION = 1
if language != new_value:
language = new_value
emit_changed()
- Configs.change_locale.call_deferred()
+ Configs.sync_locale.call_deferred()
Configs.language_changed.emit.call_deferred()
# Theming
@@ -179,7 +179,7 @@ const CURRENT_VERSION = 1
if background_color != new_value:
background_color = new_value
emit_changed()
- Configs.change_background_color.call_deferred()
+ Configs.sync_background_color.call_deferred()
@export var grid_color := Color(0.5, 0.5, 0.5):
set(new_value):
@@ -571,7 +571,7 @@ func erase_shortcut_panel_slot(slot: int) -> void:
Configs.shortcut_panel_changed.emit()
-const MAX_TABS = 50
+const MAX_TABS = 4096
@export var _tabs: Array[TabData] = []:
set(new_value):
# Validation
@@ -657,6 +657,7 @@ func set_active_tab_index(new_index: int) -> void:
if old_id != _tabs[_active_tab_index].id:
Configs.active_tab_changed.emit()
+# Basic operation that all tab adding methods call.
func _add_new_tab() -> void:
if _tabs.size() >= MAX_TABS:
return
@@ -689,7 +690,8 @@ func add_empty_tab() -> void:
Configs.tabs_changed.emit()
set_active_tab_index(_tabs.size() - 1)
-# Adds a new path with the given path, unless something with the path already exists.
+# Adds a new path with the given path.
+# If a tab with the path already exists, it's set as the active tab instead.
func add_tab_with_path(new_file_path: String) -> void:
for idx in _tabs.size():
if _tabs[idx].svg_file_path == new_file_path:
@@ -701,6 +703,8 @@ func add_tab_with_path(new_file_path: String) -> void:
Configs.tabs_changed.emit()
set_active_tab_index(_tabs.size() - 1)
+# Note that a method for removing multiple tabs at once isn't straightforward,
+# since removed tabs can show dialogs asking the user if they should be saved.
func remove_tab(idx: int) -> void:
if idx < 0 or idx >= _tabs.size():
return
diff --git a/src/config_classes/TabData.gd b/src/config_classes/TabData.gd
index 77c8577..dc7879f 100644
--- a/src/config_classes/TabData.gd
+++ b/src/config_classes/TabData.gd
@@ -116,18 +116,9 @@ func get_edited_file_path() -> String:
static func get_edited_file_path_for_id(checked_id: int) -> String:
return "%s/save%d.svg" % [EDITED_FILES_DIR, checked_id]
-# Method for showing the file path without stuff like "/home/mewpurpur/".
-# This information is pretty much always unnecessary clutter.
+
func get_presented_svg_file_path() -> String:
- var home_dir: String
- if OS.get_name() == "Windows":
- home_dir = OS.get_environment("USERPROFILE")
- else:
- home_dir = OS.get_environment("HOME")
-
- if svg_file_path.begins_with(home_dir):
- return svg_file_path.trim_prefix(home_dir).trim_prefix("/").trim_prefix("\\")
- return svg_file_path
+ return Utils.simplify_file_path(svg_file_path)
func undo() -> void:
@@ -142,7 +133,7 @@ func redo() -> void:
func _on_language_changed() -> void:
- if svg_file_path.is_empty():
+ if not is_saved():
queue_sync()
func queue_sync() -> void:
@@ -154,7 +145,7 @@ func _sync() -> void:
return
_sync_pending = false
- if not svg_file_path.is_empty():
+ if is_saved():
# The extension is included in the presented name too.
# It's always in the end anyway so it can't hide useless info.
# And also, it prevents ".svg" from being presented as an empty string.
@@ -194,3 +185,9 @@ func deactivate() -> void:
func get_true_svg_text() -> String:
return _svg_text if active else FileAccess.get_file_as_string(get_edited_file_path())
+
+func is_empty() -> bool:
+ return empty_unsaved
+
+func is_saved() -> bool:
+ return not svg_file_path.is_empty()
diff --git a/src/data_classes/Attribute.gd b/src/data_classes/Attribute.gd
index f6b3cf6..c0d9c80 100644
--- a/src/data_classes/Attribute.gd
+++ b/src/data_classes/Attribute.gd
@@ -4,6 +4,8 @@ class_name Attribute extends RefCounted
signal value_changed
+enum NameValidityLevel {VALID, INVALID_XML_NAMETOKEN, INVALID}
+
var name: String
var _value: String
@@ -35,3 +37,24 @@ func _format(text: String, _formatter: Formatter) -> String:
func _init(new_name: String, init_value := "") -> void:
name = new_name
set_value(init_value)
+
+static func get_name_validity(id: String) -> NameValidityLevel:
+ var validity_level := NameValidityLevel.VALID
+ for id_char in id:
+ if id_char in ":_-.":
+ continue
+ var u := id_char.unicode_at(0)
+ if (u >= 48 and u <= 57) or (u >= 65 and u <= 90) or (u >= 97 and u <= 122) or\
+ (u >= 0xC0 and u <= 0xD6) or (u >= 0xD8 and u <= 0xF6) or\
+ (u >= 0xF8 and u <= 0x2FF) or (u >= 0x370 and u <= 0x37D) or\
+ (u >= 0x37F and u <= 0x1FFF) or (u >= 0x200C and u <= 0x200D) or\
+ (u >= 0x2070 and u <= 0x218F) or (u >= 0x2C00 and u <= 0x2FEF) or\
+ (u >= 0x3001 and u <= 0xD7FF) or (u >= 0xF900 and u <= 0xFDCF) or\
+ (u >= 0xFDF0 and u <= 0xFFFD) or (u >= 0x10000 and u <= 0xEFFFF):
+ continue
+
+ if id_char in " \n\r\t\r":
+ return NameValidityLevel.INVALID
+ else:
+ validity_level = NameValidityLevel.INVALID_XML_NAMETOKEN
+ return validity_level
diff --git a/src/data_classes/AttributeHref.gd b/src/data_classes/AttributeHref.gd
new file mode 100644
index 0000000..cdcb7c7
--- /dev/null
+++ b/src/data_classes/AttributeHref.gd
@@ -0,0 +1,13 @@
+# An attribute representing an element's href
+class_name AttributeHref extends Attribute
+
+func set_value(new_value: String) -> void:
+ super(new_value if get_validity(new_value) != NameValidityLevel.INVALID else "")
+
+
+static func get_validity(id: String) -> NameValidityLevel:
+ if id.is_empty():
+ return NameValidityLevel.INVALID
+
+ # Allow '#'.
+ return get_name_validity(id.trim_prefix("#"))
diff --git a/src/data_classes/AttributeHref.gd.uid b/src/data_classes/AttributeHref.gd.uid
new file mode 100644
index 0000000..ad19a62
--- /dev/null
+++ b/src/data_classes/AttributeHref.gd.uid
@@ -0,0 +1 @@
+uid://dwywmm3ljqfrg
diff --git a/src/data_classes/AttributeID.gd b/src/data_classes/AttributeID.gd
index 5b624dc..e769b3c 100644
--- a/src/data_classes/AttributeID.gd
+++ b/src/data_classes/AttributeID.gd
@@ -1,32 +1,12 @@
# An attribute representing an element's id
class_name AttributeID extends Attribute
-enum ValidityLevel {VALID, INVALID_XML_NAMETOKEN, INVALID}
-
func set_value(new_value: String) -> void:
- super(new_value if get_validity(new_value) != ValidityLevel.INVALID else "")
+ super(new_value if get_validity(new_value) != NameValidityLevel.INVALID else "")
-static func get_validity(id: String) -> ValidityLevel:
+static func get_validity(id: String) -> NameValidityLevel:
if id.is_empty() or id[0] == "#":
- return ValidityLevel.INVALID
+ return NameValidityLevel.INVALID
- var validity_level := ValidityLevel.VALID
- for id_char in id:
- if id_char in ":_-.":
- continue
- var u := id_char.unicode_at(0)
- if (u >= 48 and u <= 57) or (u >= 65 and u <= 90) or (u >= 97 and u <= 122) or\
- (u >= 0xC0 and u <= 0xD6) or (u >= 0xD8 and u <= 0xF6) or\
- (u >= 0xF8 and u <= 0x2FF) or (u >= 0x370 and u <= 0x37D) or\
- (u >= 0x37F and u <= 0x1FFF) or (u >= 0x200C and u <= 0x200D) or\
- (u >= 0x2070 and u <= 0x218F) or (u >= 0x2C00 and u <= 0x2FEF) or\
- (u >= 0x3001 and u <= 0xD7FF) or (u >= 0xF900 and u <= 0xFDCF) or\
- (u >= 0xFDF0 and u <= 0xFFFD) or (u >= 0x10000 and u <= 0xEFFFF):
- continue
-
- if id_char in " \n\r\t\r":
- return ValidityLevel.INVALID
- else:
- validity_level = ValidityLevel.INVALID_XML_NAMETOKEN
- return validity_level
+ return get_name_validity(id)
diff --git a/src/data_classes/AttributeTransformList.gd b/src/data_classes/AttributeTransformList.gd
index d71908f..6bedb0c 100644
--- a/src/data_classes/AttributeTransformList.gd
+++ b/src/data_classes/AttributeTransformList.gd
@@ -123,15 +123,15 @@ static func text_to_transform_list(text: String) -> Array[Transform]:
@warning_ignore("shadowed_global_identifier")
var char := transform_params[idx]
- if comma_exhausted and not char in " \n\t\r":
- comma_exhausted = false
-
var start_idx := idx
var end_idx := idx
var number_proceed := true
var passed_decimal_point := false
var exponent_just_passed := true
while number_proceed and idx < transform_params.length():
+ if comma_exhausted and not char in " \n\t\r":
+ comma_exhausted = false
+
char = transform_params[idx]
match char:
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
diff --git a/src/data_classes/ColorParser.gd b/src/data_classes/ColorParser.gd
index f3107e2..cc2cc5f 100644
--- a/src/data_classes/ColorParser.gd
+++ b/src/data_classes/ColorParser.gd
@@ -95,7 +95,7 @@ static func is_valid_url(color: String) -> bool:
color = color.strip_edges()
if not color.begins_with("url(") or not color.ends_with(")"):
return false
- return AttributeID.get_validity(_get_url_id(color)) != AttributeID.ValidityLevel.INVALID
+ return AttributeID.get_validity(_get_url_id(color)) != Attribute.NameValidityLevel.INVALID
static func _get_url_id(stripped_color: String) -> String:
return stripped_color.substr(4,
diff --git a/src/data_classes/DB.gd b/src/data_classes/DB.gd
index c8bcdf2..08e1ca2 100644
--- a/src/data_classes/DB.gd
+++ b/src/data_classes/DB.gd
@@ -1,12 +1,13 @@
class_name DB extends RefCounted
-enum AttributeType {NUMERIC, COLOR, LIST, PATHDATA, ENUM, TRANSFORM_LIST, ID, UNKNOWN}
+enum AttributeType {NUMERIC, COLOR, LIST, PATHDATA, ENUM, TRANSFORM_LIST, ID, HREF, UNKNOWN}
enum PercentageHandling {FRACTION, HORIZONTAL, VERTICAL, NORMALIZED}
enum NumberRange {ARBITRARY, POSITIVE, UNIT}
const recognized_elements: Array[String] = ["svg", "g", "circle", "ellipse", "rect",
- "path", "line", "polyline", "polygon", "stop", "linearGradient", "radialGradient"]
+ "path", "line", "polyline", "polygon", "stop", "linearGradient", "radialGradient",
+ "use"]
const _element_icons: Dictionary[String, Texture2D] = {
"circle": preload("res://assets/icons/element/circle.svg"),
@@ -21,6 +22,7 @@ const _element_icons: Dictionary[String, Texture2D] = {
"linearGradient": preload("res://assets/icons/element/linearGradient.svg"),
"radialGradient": preload("res://assets/icons/element/radialGradient.svg"),
"stop": preload("res://assets/icons/element/stop.svg"),
+ "use": preload("res://assets/icons/element/use.svg"),
}
const _unrecognized_xnode_icon = preload("res://assets/icons/element/unrecognized.svg")
@@ -57,13 +59,14 @@ const recognized_attributes: Dictionary[String, Array] = {
"polyline": ["transform", "opacity", "fill", "fill-opacity", "stroke",
"stroke-opacity", "stroke-width", "stroke-linecap", "stroke-linejoin", "points"],
"stop": ["offset", "stop-color", "stop-opacity"],
+ "use": ["href", "transform", "x", "y"]
}
const _valid_children: Dictionary[String, Array] = {
"svg": ["svg", "path", "circle", "ellipse", "rect", "line", "polygon", "polyline",
- "g", "linearGradient", "radialGradient"],
+ "g", "linearGradient", "radialGradient", "use"],
"g": ["svg", "path", "circle", "ellipse", "rect", "line", "polygon", "polyline",
- "g", "linearGradient", "radialGradient"],
+ "g", "linearGradient", "radialGradient", "use"],
"linearGradient": ["stop"],
"radialGradient": ["stop"],
"circle": [],
@@ -74,6 +77,7 @@ const _valid_children: Dictionary[String, Array] = {
"polygon": [],
"polyline": [],
"stop": [],
+ "use": [],
}
const propagated_attributes: Array[String] = ["fill", "fill-opacity", "stroke",
@@ -113,6 +117,7 @@ const _attribute_types: Dictionary[String, AttributeType] = {
"gradientTransform": AttributeType.TRANSFORM_LIST,
"gradientUnits": AttributeType.ENUM,
"spreadMethod": AttributeType.ENUM,
+ "href": AttributeType.HREF,
}
const attribute_enum_values: Dictionary[String, Array] = {
@@ -218,6 +223,7 @@ static func element(name: String) -> Element:
"linearGradient": return ElementLinearGradient.new()
"radialGradient": return ElementRadialGradient.new()
"stop": return ElementStop.new()
+ "use": return ElementUse.new()
_: return ElementUnrecognized.new(name)
static func attribute(name: String, value: String) -> Attribute:
@@ -229,6 +235,7 @@ static func attribute(name: String, value: String) -> Attribute:
DB.AttributeType.ENUM: return AttributeEnum.new(name, value)
DB.AttributeType.TRANSFORM_LIST: return AttributeTransformList.new(name, value)
DB.AttributeType.ID: return AttributeID.new(name, value)
+ DB.AttributeType.HREF: return AttributeHref.new(name, value)
_: return Attribute.new(name, value)
diff --git a/src/data_classes/Element.gd b/src/data_classes/Element.gd
index 9525162..554fb7f 100644
--- a/src/data_classes/Element.gd
+++ b/src/data_classes/Element.gd
@@ -60,6 +60,14 @@ func get_all_element_descendants() -> Array[Element]:
elements += child.get_all_element_descendants()
return elements
+func get_all_valid_element_descendants() -> Array[Element]:
+ var elements: Array[Element] = []
+ for child in get_children():
+ if child.is_element() and DB.is_child_element_valid(self.name, child.name):
+ elements.append(child)
+ elements += child.get_all_valid_element_descendants()
+ return elements
+
# Gets the basic XML nodes too.
func get_all_xnode_descendants() -> Array[XNode]:
var xnodes: Array[XNode] = []
diff --git a/src/data_classes/ElementRoot.gd b/src/data_classes/ElementRoot.gd
index 61e9504..cd5734f 100644
--- a/src/data_classes/ElementRoot.gd
+++ b/src/data_classes/ElementRoot.gd
@@ -28,7 +28,7 @@ func get_xnode(loc: PackedInt32Array) -> XNode:
return current_element
func get_element_by_id(id: String) -> Element:
- for element in get_all_element_descendants():
+ for element in get_all_valid_element_descendants():
if element.get_attribute_value("id") == id:
return element
return null
diff --git a/src/data_classes/ElementSVG.gd b/src/data_classes/ElementSVG.gd
index 463aef4..25dde34 100644
--- a/src/data_classes/ElementSVG.gd
+++ b/src/data_classes/ElementSVG.gd
@@ -1,9 +1,6 @@
# An